├── package ├── hooks_generator │ ├── __tests__ │ │ ├── hooks-component.test.js │ │ ├── hooks-output.test.js │ │ └── hooks-api.test.js │ └── hooks_src │ │ ├── hooks-types.ts │ │ ├── utils │ │ ├── hooks-ledger.ts │ │ └── hooks-store.ts │ │ ├── output │ │ ├── hooks-output.ts │ │ └── hooks-output-utils.ts │ │ ├── component │ │ ├── hooks-component-utils.ts │ │ └── HooksChromogenObserver.tsx │ │ └── api │ │ ├── hooks-api.ts │ │ └── hooks-core-utils.ts ├── babel.config.js ├── recoil_generator │ ├── src │ │ ├── utils │ │ │ ├── store.ts │ │ │ ├── ledger.ts │ │ │ └── utils.ts │ │ ├── api │ │ │ ├── family-utils.ts │ │ │ ├── core-utils.ts │ │ │ └── api.ts │ │ ├── output │ │ │ ├── output.ts │ │ │ └── output-utils.ts │ │ ├── types.ts │ │ └── component │ │ │ ├── component-utils.ts │ │ │ └── ChromogenObserver.tsx │ └── __tests__ │ │ ├── core-utils.test.js │ │ ├── component-utils.test.js │ │ ├── output.test.js │ │ ├── utils.test.js │ │ ├── api.test.js │ │ ├── component.test.js │ │ └── output-utils.test.js ├── index.ts ├── LICENSE ├── package.json ├── tsconfig.json └── README.md ├── assets ├── devtool.gif ├── logo │ ├── React-icon.png │ ├── test-tube.png │ ├── chromogen-logo.png │ └── chromogen-banner.png ├── README-demo │ └── demo-app.png └── README-root │ ├── buttons.png │ ├── download.png │ ├── test-output.png │ ├── devtool-panel.png │ ├── filepath-after.png │ ├── filepath-before.png │ └── test-directory.png ├── demo-todo ├── src │ ├── favicon.ico │ ├── index.js │ ├── components │ │ ├── App.jsx │ │ ├── Quotes.jsx │ │ ├── TodoQuickCheck.jsx │ │ ├── ReadOnlyTodoItem.jsx │ │ ├── TodoList.jsx │ │ ├── SearchBar.jsx │ │ ├── TodoItem.jsx │ │ ├── TodoListFilters.jsx │ │ └── TodoItemCreator.jsx │ ├── index.html │ ├── store │ │ ├── atoms.js │ │ └── store.js │ └── styles │ │ └── styles.css ├── __tests__ │ └── example.test.js ├── .babelrc ├── README.md ├── webpack.config.js ├── LICENSE └── package.json ├── dev-tool ├── build │ ├── imgs │ │ └── chromogen-logo.png │ ├── devtools.js │ ├── devtools.html │ ├── manifest.json │ ├── panel.html │ └── styles │ │ └── styles.css ├── types │ └── types.ts ├── app │ ├── index.tsx │ └── Components │ │ ├── Recorder.tsx │ │ └── App.tsx ├── content.ts ├── README.md ├── tsconfig.json ├── webpack.config.js ├── background.ts └── package.json ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── .travis.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── package.json ├── .eslintrc.json ├── CODE_OF_CONDUCT.md └── README.md /package/hooks_generator/__tests__/hooks-component.test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package/hooks_generator/__tests__/hooks-output.test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/devtool.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/devtool.gif -------------------------------------------------------------------------------- /assets/logo/React-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/logo/React-icon.png -------------------------------------------------------------------------------- /assets/logo/test-tube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/logo/test-tube.png -------------------------------------------------------------------------------- /demo-todo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/demo-todo/src/favicon.ico -------------------------------------------------------------------------------- /assets/README-demo/demo-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/README-demo/demo-app.png -------------------------------------------------------------------------------- /assets/README-root/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/README-root/buttons.png -------------------------------------------------------------------------------- /assets/README-root/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/README-root/download.png -------------------------------------------------------------------------------- /assets/logo/chromogen-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/logo/chromogen-logo.png -------------------------------------------------------------------------------- /assets/README-root/test-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/README-root/test-output.png -------------------------------------------------------------------------------- /assets/logo/chromogen-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/logo/chromogen-banner.png -------------------------------------------------------------------------------- /assets/README-root/devtool-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/README-root/devtool-panel.png -------------------------------------------------------------------------------- /assets/README-root/filepath-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/README-root/filepath-after.png -------------------------------------------------------------------------------- /assets/README-root/filepath-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/README-root/filepath-before.png -------------------------------------------------------------------------------- /assets/README-root/test-directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/assets/README-root/test-directory.png -------------------------------------------------------------------------------- /demo-todo/__tests__/example.test.js: -------------------------------------------------------------------------------- 1 | test('Jest can run tests in __tests__ folder', () => { 2 | expect(true).toBe(true); 3 | }); 4 | -------------------------------------------------------------------------------- /dev-tool/build/imgs/chromogen-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtumel123/Chromogen/HEAD/dev-tool/build/imgs/chromogen-logo.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/package" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /demo-todo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env"], 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "react-hot-loader/babel" 8 | ] 9 | } -------------------------------------------------------------------------------- /dev-tool/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface Connections { 2 | [tabId: string]: any; 3 | } 4 | 5 | export interface Message { 6 | tabId: string; 7 | action: string; 8 | } 9 | -------------------------------------------------------------------------------- /package/hooks_generator/__tests__/hooks-api.test.js: -------------------------------------------------------------------------------- 1 | // import {ledger} from '../hooks_src/utils/hooks-ledger'; 2 | // import {useState} from '../hooks_src/api/hooks-api'; 3 | -------------------------------------------------------------------------------- /package/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-react', 5 | '@babel/preset-typescript', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | env: 5 | - TEST_DIR=package 6 | before_install: 7 | - cd $TEST_DIR 8 | install: 9 | - npm install 10 | script: 11 | - npm run test 12 | - npm run coveralls 13 | -------------------------------------------------------------------------------- /package/hooks_generator/hooks_src/hooks-types.ts: -------------------------------------------------------------------------------- 1 | // Defining type for our hooks-ledger 2 | export interface Ledger { 3 | state: any; 4 | id: string | number; 5 | initialState: any; 6 | currState: any; 7 | dispCount: number; 8 | } 9 | -------------------------------------------------------------------------------- /demo-todo/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import App from './components/App'; 5 | 6 | render(, document.getElementById('app')); 7 | -------------------------------------------------------------------------------- /dev-tool/app/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import App from './Components/App'; 5 | /* eslint-enable */ 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | 4 | # System files 5 | .DS_Store 6 | .vscode 7 | 8 | # Tests 9 | coverage 10 | 11 | # Package 12 | package/build 13 | 14 | # DevTool 15 | dev-tool/build/bundles 16 | 17 | # Demo App 18 | chromogen.test.js -------------------------------------------------------------------------------- /dev-tool/build/devtools.js: -------------------------------------------------------------------------------- 1 | /* This file is included as a script tag in devtools.html. 2 | Its only purpose is to create the devtools panel. */ 3 | chrome.devtools.panels.create('Chromogen', null, 'panel.html', null); 4 | // args - panel title, icon, html, and a callback function. 5 | -------------------------------------------------------------------------------- /package/hooks_generator/hooks_src/utils/hooks-ledger.ts: -------------------------------------------------------------------------------- 1 | import {Ledger} from '../hooks-types' 2 | 3 | // Storing initialState, currState, and prevState but through the store 4 | export const hooksLedger: Ledger = { 5 | state: [], 6 | id: '', 7 | initialState: '', 8 | currState: '', 9 | dispCount: 0, 10 | }; -------------------------------------------------------------------------------- /package/recoil_generator/src/utils/store.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { RecoilState } from 'recoil'; 3 | import { atom } from 'recoil'; 4 | /* eslint-enable */ 5 | 6 | // Recording toggle 7 | export const recordingState: RecoilState = atom({ 8 | key: 'recordingState', 9 | default: true, 10 | }); 11 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "always", 13 | "endOfLine": "lf" 14 | } 15 | -------------------------------------------------------------------------------- /dev-tool/build/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chromogen 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/core-utils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | debouncedAddToTransactions, 3 | wrapGetter, 4 | wrapSetter, 5 | } from '../src/api/core-utils'; 6 | 7 | import { debounce } from '../src/utils/utils'; 8 | 9 | describe('debouncedAddToTransaction', () => { 10 | 11 | }); 12 | 13 | describe('wrapGetter', () => { 14 | 15 | }); 16 | 17 | describe('wrapSetter', () => { 18 | 19 | }); -------------------------------------------------------------------------------- /demo-todo/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecoilRoot } from 'recoil'; 3 | import { ChromogenObserver } from 'chromogen'; 4 | import TodoList from './TodoList'; 5 | import * as selectors from '../store/store'; 6 | import * as atoms from '../store/atoms'; 7 | 8 | const App = () => ( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /package/hooks_generator/hooks_src/utils/hooks-store.ts: -------------------------------------------------------------------------------- 1 | import React, { Reducer } from 'react'; 2 | import { Store } from 'redux'; 3 | 4 | type UnsubscribeFn = () => void; 5 | 6 | export type EnhancedStore = Store & { 7 | registerHookedReducer: ( 8 | reducer: Reducer, 9 | initialState: any, 10 | reducerId: string | number, 11 | ) => UnsubscribeFn; 12 | }; 13 | 14 | 15 | export const ObserverContext = React.createContext(undefined); 16 | -------------------------------------------------------------------------------- /dev-tool/content.ts: -------------------------------------------------------------------------------- 1 | /* This file serves as an intermediary between the Chromogen package 2 | and background.js. (background.js can communicate with DevTools page) 3 | */ 4 | 5 | // Relay messages from package to background.js (-> DevTools panel) 6 | window.addEventListener('message', (message) => chrome.runtime.sendMessage(message.data)); 7 | 8 | // Relay messages from background.js (DevTools panel listener) to package 9 | chrome.runtime.onMessage.addListener((message) => window.postMessage(message, '*')); 10 | -------------------------------------------------------------------------------- /package/recoil_generator/src/utils/ledger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Ledger } from '../types'; 3 | import { RecoilState, SerializableParam } from 'recoil'; 4 | /* eslint-enable */ 5 | 6 | export const ledger: Ledger, any, SerializableParam> = { 7 | atoms: [], 8 | selectors: [], //get 9 | atomFamilies: {}, 10 | selectorFamilies: {}, 11 | setters: [], //set 12 | initialRender: [], 13 | initialRenderFamilies: [], 14 | transactions: [],//get 15 | setTransactions: [],//set 16 | }; 17 | -------------------------------------------------------------------------------- /demo-todo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Chromogen To-Do Demo 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /dev-tool/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Chromogen", 3 | "version" : "0.1.0", 4 | "manifest_version" : 2, 5 | "description" : "DevTool panel for Chromogen, a UX-driven test generator for Recoil apps.", 6 | "author" : "Chromogen Team", 7 | "icons": { "128": "./imgs/chromogen-logo.png"}, 8 | "devtools_page" : "devtools.html", 9 | "background": { 10 | "persistent": false, 11 | "scripts": ["bundles/background.bundle.js"] 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": [""], 16 | "js": ["bundles/content.bundle.js"] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /package/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { atom, selector, atomFamily, selectorFamily } from './recoil_generator/src/api/api'; 3 | import { ChromogenObserver } from './recoil_generator/src/component/ChromogenObserver'; 4 | import { useState } from './hooks_generator/hooks_src/api/hooks-api' 5 | import { HooksChromogenObserver } from './hooks_generator/hooks_src/component/HooksChromogenObserver'; 6 | // CHROMGOEN FAMILY APIs ARE CURRENTLY UNSTABLE 7 | // import { atomFamily, selectorFamily } from 'recoil'; 8 | export { atom, selector, atomFamily, selectorFamily, HooksChromogenObserver, ChromogenObserver, useState }; 9 | -------------------------------------------------------------------------------- /dev-tool/build/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Chromogen 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo-todo/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # The official demo app for [Chromogen](https://github.com/oslabs-beta/Chromogen). 4 | 5 | ![demo app interface](../assets/README-demo/demo-app.png) 6 | 7 |
8 | 9 | ## Selector Implementations 10 | - Readonly: 11 | 1. displayed todo list items, based on filter selection (sort & active/complete) 12 | 2. stats (priority count and active/complete counts) 13 | 3. displayed todo list empty / non-empty 14 | - Writeable: 15 | 1. "all complete" checkbox toggle 16 | 1. reset filter states 17 | - Promise: 18 | 1. quote text 19 | - Async / Await: 20 | 1. xkcd comic 21 | - selectorFamily (_in progress_): 22 | 1. search bar -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /package/hooks_generator/hooks_src/output/hooks-output.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { Ledger } from '../hooks-types'; 3 | import { importHooksId, testState } from './hooks-output-utils'; 4 | /* eslint-enable */ 5 | 6 | // NOTE: HooksOutput needs a beforeEach to bring down state (to instantiate state and create mock data for testing) 7 | export const hooksOutput = ({ 8 | state, 9 | id 10 | }: Ledger): any => 11 | `import { renderHook } from '@testing-library/react-hooks'; 12 | import React, { useState } from 'react'; 13 | import { 14 | ${importHooksId(id)} 15 | 16 | } from ''; 17 | 18 | describe('USESTATE', () => { 19 | 20 | it(${testState(state, id)}); 21 | 22 | });`; 23 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/component-utils.test.js: -------------------------------------------------------------------------------- 1 | import { ledger } from '../src/utils/ledger.ts'; 2 | import {generateFile} from '../src/component/component-utils'; 3 | 4 | // Testing generateFile 5 | describe('generateFile', () => { 6 | const setFile = 0 7 | const array = [[], [], []] 8 | let storeMap = new Map(array); 9 | 10 | const { 11 | atoms, 12 | selectors, 13 | setters, 14 | atomFamilies, 15 | selectorFamilies, 16 | initialRender, 17 | initialRenderFamilies, 18 | transactions, 19 | setTransactions, 20 | } = ledger; 21 | 22 | // We expect our generate file to not be the falsy return statement, which is the entirety of the ledger, with atoms being the new user input 23 | generateFile(setFile, storeMap) 24 | }) 25 | -------------------------------------------------------------------------------- /dev-tool/README.md: -------------------------------------------------------------------------------- 1 | ## Chromogen 2.0 is Now Compatible with useState Hook Testing! 2 | 3 | DevTool panel for Chromogen, a UX-driven test generator for Recoil and useState applications. 4 | 5 | 6 | ## Getting Started 7 | Please install [Chromogen](#https://chrome.google.com/webstore/detail/chromogen/cciblhdjhpdbpeenlnnhccooheamamnd) from the Chrome Web Store. 8 | 9 | 10 | ### You must download Chromogen package from npm before using our Dev Tool. 11 | 12 | ``` 13 | npm install chromogen 14 | ``` 15 | 16 | 17 | ## Chrome Extension Demo 18 | 19 | 20 | ![devtool](../assets/devtool.gif) 21 | 22 | 23 | ## Version History 24 | 25 | * 2.0 26 | * Newest version of Chromogen now compatible with useState Hooks, can generate 27 | * 1.0 28 | * Initial Release compatible with Recoil -------------------------------------------------------------------------------- /dev-tool/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", // path to output directory 4 | "sourceMap": true, // allow sourcemap support 5 | "strictNullChecks": true, // enable strict null checks as a best practice 6 | "removeComments": true, // remove comments from output 7 | "module": "commonjs", // specify module code generation 8 | "jsx": "react", // use typescript to transpile jsx to js 9 | "target": "es5", // specify ECMAScript target version 10 | "allowJs": true, // allow a partial TypeScript and JavaScript codebase 11 | "esModuleInterop": true // enable flexible import syntax 12 | }, 13 | "include": [ 14 | "./" 15 | ], 16 | "exclude": ["node_modules", ".vscode"] 17 | } -------------------------------------------------------------------------------- /demo-todo/src/components/Quotes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { quoteTextState, xkcdState } from '../store/store'; 4 | import { quoteNumberState } from '../store/atoms'; 5 | 6 | const Quotes = () => { 7 | const setQuoteNumber = useSetRecoilState(quoteNumberState); 8 | const quoteText = useRecoilValue(quoteTextState); 9 | const xkcdURL = useRecoilValue(xkcdState); 10 | 11 | return ( 12 | <> 13 |
14 |

{quoteText}

15 | 18 |
19 | xkcd 20 | 21 | ); 22 | }; 23 | 24 | export default Quotes; 25 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoQuickCheck.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue } from 'recoil'; 3 | import Checkbox from '@material-ui/core/Checkbox'; 4 | import { allCompleteState, filteredListContentState } from '../store/store'; 5 | 6 | const TodoQuickCheck = () => { 7 | const [allComplete, setAllComplete] = useRecoilState(allCompleteState); 8 | const display = useRecoilValue(filteredListContentState); 9 | 10 | return ( 11 | display && ( 12 |
13 | setAllComplete(!allComplete)} 19 | /> 20 | all 21 |
22 | ) 23 | ); 24 | }; 25 | 26 | export default TodoQuickCheck; 27 | -------------------------------------------------------------------------------- /demo-todo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, './src/index.js'), 5 | output: { 6 | filename: 'bundle.js', 7 | }, 8 | devServer: { 9 | contentBase: path.resolve(__dirname, './src'), 10 | historyApiFallback: true, 11 | }, 12 | mode: process.env.NODE_ENV, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | exclude: /node_modules/, 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-env', '@babel/preset-react'], 21 | plugins: ['@babel/transform-runtime'], 22 | }, 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | { 28 | loader: 'style-loader', 29 | }, 30 | { 31 | loader: 'css-loader', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }, 37 | resolve: { 38 | extensions: ['.js', '.jsx'], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /demo-todo/src/components/ReadOnlyTodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@material-ui/core/Checkbox'; 3 | import '../styles/styles.css'; 4 | import { todoListState } from '../store/atoms'; 5 | import { useRecoilValue } from 'recoil'; 6 | 7 | const ReadOnlyTodoItem = ({ item }) => { 8 | const checkBoxClasses = { 9 | low: 'lowPriority', 10 | medium: 'mediumPriority', 11 | high: 'highPriority', 12 | }; 13 | 14 | const todoList = useRecoilValue(todoListState); 15 | 16 | return todoList.find((todo) => todo.id === item.id) ? ( 17 |
18 | 19 | todo.id === item.id).isComplete} 22 | color="default" 23 | inputProps={{ 'aria-label': 'primary checkbox' }} 24 | style={{ cursor: 'default' }} 25 | /> 26 |
27 | ) : null; 28 | }; 29 | export default ReadOnlyTodoItem; 30 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Types of changes 2 | 3 | - [ ] Bugfix (change which fixes an issue) 4 | - [ ] New feature (change which adds functionality) 5 | - [ ] Refactor (change which changes the codebase without affecting its external behavior) 6 | - [ ] Non-breaking change (fix or feature that would causes existing functionality to work as expected) 7 | - [ ] Breaking change (fix or feature that would cause existing functionality to __not__ work as expected) 8 | ## Purpose 9 | 10 | ## Approach 11 | 12 | ## Resources 13 | 14 | ## Screenshot(s) 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /package/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /demo-todo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michelle Holland 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 | -------------------------------------------------------------------------------- /package/hooks_generator/hooks_src/output/hooks-output-utils.ts: -------------------------------------------------------------------------------- 1 | // Output functions here are used inside hooks-output-utils as regex inside the user's exported test file 2 | 3 | // Import hooks state from user's app 4 | export function importHooksId(id: string | number) { 5 | return `${id}`; 6 | } 7 | 8 | // Tests whether current state is NOT null or undefined 9 | export function testState(state: any, id: string | number) { 10 | return `'should show that state in ${id} is not null or undefined', () => { 11 | expect(${state.length-1}).not.toBe(undefined); 12 | expect(${state.length-1}).not.toBe(null)); 13 | }` 14 | } 15 | 16 | // Testing that individual state is changing, and our ledger is updating accordingly 17 | // BUG: State needs to be a primitive data type or mapped over in order for the test to be read. Arrays and objects will show up as undefined 18 | 19 | // export function testStateChange (state: any, id: string | number, dispCount: number) { 20 | // return `'should show that state in ${id} changes after every dispatch and its length should be equal to dispatch count', () => { 21 | // expect(${state[dispCount-1]}).not.toBe(${state[dispCount-2]})); 22 | // expect(${state.length}).toBe(${dispCount}); 23 | // }` 24 | // } -------------------------------------------------------------------------------- /demo-todo/src/store/atoms.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'chromogen'; 2 | 3 | /* ----- ATOMS ----- */ 4 | 5 | // unsorted, unfiltered todo list 6 | const todoListState = atom({ 7 | key: 'mismatchTodoList', 8 | default: [], // array of objects - each object has id, text, isComplete, and priority props 9 | }); 10 | 11 | // filter select 12 | const todoListFilterState = atom({ 13 | key: 'todoListFilterState', 14 | default: 'Show All', 15 | }); 16 | 17 | // toggle sort 18 | const todoListSortState = atom({ 19 | key: 'todoListSortState', 20 | default: false, 21 | }); 22 | 23 | // random number for fetching quote & comic 24 | const quoteNumberState = atom({ 25 | key: 'quoteNumberState', 26 | default: Math.floor(Math.random() * 1643), 27 | }); 28 | 29 | const searchResultState = atom({ 30 | key: 'searchResultState', 31 | default: { 32 | all: { 33 | searchTerm: '', 34 | results: [], 35 | }, 36 | high: { 37 | searchTerm: '', 38 | results: [], 39 | }, 40 | medium: { 41 | searchTerm: '', 42 | results: [], 43 | }, 44 | low: { 45 | searchTerm: '', 46 | results: [], 47 | }, 48 | }, 49 | }); 50 | 51 | export { 52 | todoListState, 53 | todoListFilterState, 54 | todoListSortState, 55 | quoteNumberState, 56 | searchResultState, 57 | }; 58 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/output.test.js: -------------------------------------------------------------------------------- 1 | import { setFilter, output } from '../src/output/output.ts'; 2 | 3 | // testing setFilter function 4 | describe('setFilter', () => { 5 | it('should remove setter keys from array of selector keys', () => { 6 | // create mock selectors array 7 | const selectors = ['one', 'two', 'three']; 8 | // create mock setters array 9 | const setters = ['one']; 10 | // store evaluated result of invoking setFilter on the mock data in an array 11 | const filtered = setFilter(selectors, setters); 12 | // verify that the info from setters caused a matching value to be removed from the selectors array ('one') 13 | expect(filtered).not.toContain('one'); 14 | }); 15 | }); 16 | 17 | describe('output', () => { 18 | it('should return a string', () => { 19 | // create mock ledger object 20 | const mockLedger = { 21 | atoms: [], 22 | selectors: [], 23 | setters: [], 24 | atomFamilies: [], 25 | selectorFamilies: [], 26 | initialRender: [], 27 | initialRenderFamilies: [], 28 | transactions: [], 29 | setTransactions: [], 30 | }; 31 | // verify that type of mockLedger is a string after output function is invoked on it 32 | expect(typeof output(mockLedger)).toEqual('string'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { sortedTodoListState } from '../store/store'; 4 | import TodoItem from './TodoItem'; 5 | import TodoItemCreator from './TodoItemCreator'; 6 | import TodoListFilters from './TodoListFilters'; 7 | import TodoQuickCheck from './TodoQuickCheck'; 8 | import Quotes from './Quotes'; 9 | import SearchBar from './SearchBar'; 10 | import '../styles/styles.css'; 11 | 12 | const TodoList = () => { 13 | const todoList = useRecoilValue(sortedTodoListState); 14 | 15 | return ( 16 |
17 |
18 | Loading...}> 19 | 20 | 21 |
22 | 23 |
24 |

Totally Todos!

25 | 26 |
27 | 28 | 29 | {todoList.map((todoItem) => ( 30 | 31 | ))} 32 | 33 |
34 | 35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default TodoList; 42 | -------------------------------------------------------------------------------- /dev-tool/app/Components/Recorder.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | import PlayArrowIcon from '@material-ui/icons/PlayArrow'; 4 | import StopIcon from '@material-ui/icons/Stop'; 5 | import GetAppIcon from '@material-ui/icons/GetApp'; 6 | 7 | const Recorder: React.FC<{ status: boolean }> = ({ status }) => { 8 | // Connect to background.js 9 | const backgroundConnection = chrome.runtime.connect(); 10 | 11 | // Send messages to background.js 12 | const sendMessage = (action: string) => { 13 | backgroundConnection.postMessage({ 14 | action, 15 | tabId: chrome.devtools.inspectedWindow.tabId, 16 | }); 17 | }; 18 | 19 | return ( 20 |
21 | 30 | 33 |
34 | ); 35 | }; 36 | 37 | export default Recorder; 38 | -------------------------------------------------------------------------------- /dev-tool/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ChromeExtensionReloader = require('webpack-chrome-extension-reloader'); 3 | 4 | const config = { 5 | entry: { 6 | app: path.resolve(__dirname, './app/index.tsx'), 7 | background: path.resolve(__dirname, './background.ts'), 8 | content: path.resolve(__dirname, './content.ts'), 9 | }, 10 | output: { 11 | path: path.join(__dirname, './build/bundles'), 12 | filename: '[name].bundle.js', 13 | }, 14 | module: { 15 | rules: [ 16 | { test: /\.(t|j)sx?$/, use: { loader: 'ts-loader' }, exclude: /node_modules/ }, 17 | { enforce: 'pre', test: /\.js$/, exclude: /node_modules/, loader: 'source-map-loader' }, 18 | { 19 | test: /\.css$/, 20 | use: [ 21 | { 22 | loader: 'style-loader', 23 | }, 24 | { 25 | loader: 'css-loader', 26 | }, 27 | ], 28 | }, 29 | ], 30 | }, 31 | resolve: { 32 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 33 | }, 34 | devtool: 'source-map', 35 | plugins: [], 36 | }; 37 | 38 | module.exports = (env, argv) => { 39 | if (argv.mode === 'development') { 40 | config.plugins.push( 41 | new ChromeExtensionReloader({ 42 | entries: { 43 | contentScript: ['app', 'content'], 44 | background: ['background'], 45 | }, 46 | }), 47 | ); 48 | } 49 | return config; 50 | }; 51 | -------------------------------------------------------------------------------- /package/hooks_generator/hooks_src/component/hooks-component-utils.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'react'; 2 | import { hooksLedger as ledger } from '../utils/hooks-ledger'; 3 | import { hooksOutput as output } from '../output/hooks-output'; 4 | 5 | // Create buttonStyles and divStyles here 6 | const hooksButtonStyle: CSSProperties = { 7 | display: 'inline-block', 8 | margin: '8px', 9 | marginLeft: '13px', 10 | padding: '0px', 11 | height: '25px', 12 | width: '65px', 13 | borderRadius: '4px', 14 | justifyContent: 'space-evenly', 15 | border: '1px', 16 | cursor: 'pointer', 17 | color: '#90d1f0', 18 | fontSize: '10px', 19 | }; 20 | 21 | const hooksDivStyle: CSSProperties = { 22 | display: 'flex', 23 | position: 'absolute', 24 | bottom: '100px', 25 | left: '100px', 26 | backgroundColor: '#aaa', 27 | borderRadius: '4px', 28 | margin: 0, 29 | padding: 0, 30 | zIndex: 999999, 31 | }; 32 | 33 | const hooksPlayStyle: CSSProperties = { 34 | boxSizing: 'border-box', 35 | marginLeft: '25px', 36 | borderStyle: 'solid', 37 | borderWidth: '7px 0px 7px 14px', 38 | }; 39 | 40 | const hooksPauseStyle: CSSProperties = { 41 | width: '14px', 42 | height: '14px', 43 | borderWidth: '0px 0px 0px 10px', 44 | borderStyle: 'double', 45 | marginLeft: '27px', 46 | }; 47 | 48 | export const hookStyles = { hooksButtonStyle, hooksDivStyle, hooksPlayStyle, hooksPauseStyle }; 49 | 50 | export const generateHooksFile = (setHooksFile: Function): void => { 51 | return setHooksFile(URL.createObjectURL(new Blob([output(ledger)]))); 52 | }; 53 | -------------------------------------------------------------------------------- /package/hooks_generator/hooks_src/api/hooks-api.ts: -------------------------------------------------------------------------------- 1 | /* useState functionality 2 | 1. User uses our useState hook in their applciation, passing in initial state and an id 3 | 2. We keep track of all changes to state in our Redux store 4 | 3. ChromogenObserver then takes data from store to be exported to our output generator files 5 | 4. Output creates file for user 6 | */ 7 | 8 | /*USESTATE WITH STORE*/ 9 | 10 | import { useHookedReducer } from './hooks-core-utils'; 11 | import { useMemo, useContext, useState as useReactState } from 'react'; 12 | import { EnhancedStore, ObserverContext } from '../utils/hooks-store'; 13 | 14 | type StateAction = S | ((s: S) => S); 15 | 16 | function stateReducer(state: S, action: StateAction): S { 17 | return typeof action === 'function' ? (action as (s: S) => S)(state) : action; 18 | } 19 | 20 | export const useState = (initialState: S | (() => S), id: string | number) => { 21 | const inspectorStore = useContext(ObserverContext); 22 | // Keeping the first values 23 | const [store, reducerId] = useMemo<[EnhancedStore | undefined, string | number]>( 24 | () => [inspectorStore, id], 25 | [], 26 | ); 27 | 28 | if (!store || !reducerId) { 29 | return useReactState(initialState); 30 | } 31 | 32 | const finalInitialState = useMemo( 33 | () => (typeof initialState === 'function' ? (initialState as () => S)() : initialState), 34 | [], 35 | ); 36 | 37 | return useHookedReducer( 38 | stateReducer, 39 | finalInitialState, 40 | //created in utils/store.ts 41 | store, 42 | //key in store 43 | reducerId, 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /demo-todo/src/components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import { searchBarSelectorFam } from '../store/store'; 4 | 5 | import ReadOnlyTodoItem from './ReadOnlyTodoItem'; 6 | 7 | const SearchBar = () => { 8 | const [searchFilter, setSearchFilter] = useState('all'); 9 | const [searchText, setSearchText] = useState(''); 10 | const [searchState, setSearchState] = useRecoilState(searchBarSelectorFam(searchFilter)); 11 | 12 | const onSearchTextChange = (e) => { 13 | setSearchText(e.target.value); 14 | setSearchState(e.target.value); 15 | }; 16 | const onSelectChange = (e) => { 17 | setSearchText(''); 18 | setSearchFilter(e.target.value); 19 | }; 20 | 21 | return ( 22 |
23 | 31 | 37 |
38 | {searchState.results.map((result, idx) => ( 39 | 40 | ))} 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default SearchBar; 47 | -------------------------------------------------------------------------------- /package/recoil_generator/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SerializableParam } from 'recoil'; 3 | import type { AtomFamilies, SelectorFamilies } from '../types'; 4 | /* eslint-enable */ 5 | 6 | // Debouncing for selector transaction updates 7 | export const debounce = (func: (...args: any[]) => any, wait: number) => { 8 | let timeout: any; 9 | 10 | return (...args: any[]) => { 11 | const timeoutCallback = () => { 12 | timeout = null; 13 | func(...args); 14 | }; 15 | 16 | clearTimeout(timeout); 17 | timeout = setTimeout(timeoutCallback, wait); 18 | }; 19 | }; 20 | 21 | // Used in key-to-variable name mapping in generateFile 22 | export function convertFamilyTrackerKeys( 23 | familyTracker: AtomFamilies, 24 | storeMap: Map, 25 | ): AtomFamilies; 26 | export function convertFamilyTrackerKeys( 27 | familyTracker: SelectorFamilies, 28 | storeMap: Map, 29 | ): SelectorFamilies; 30 | 31 | export function convertFamilyTrackerKeys( 32 | familyTracker: AtomFamilies | SelectorFamilies, 33 | storeMap: Map, 34 | ) { 35 | const refactoredTracker: AtomFamilies | SelectorFamilies = {}; 36 | 37 | Object.keys(familyTracker).forEach((key) => { 38 | const newKey: string = storeMap.get(key) || key; 39 | refactoredTracker[newKey] = familyTracker[key]; 40 | }); 41 | 42 | return refactoredTracker; 43 | } 44 | 45 | // Dummy param for use in various checks (most notably the key-to-variable name mapping) 46 | export const dummyParam = 'chromogenDummyParam'; 47 | -------------------------------------------------------------------------------- /dev-tool/background.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Connections, Message } from './types/types'; 3 | /* eslint-enable */ 4 | 5 | // Listens for events from DevTools panel and content.js (package intermediary) 6 | const connections: Connections = {}; 7 | 8 | chrome.runtime.onConnect.addListener((port) => { 9 | // Listen for messages from DevTools panel 10 | const extensionListener = (message: Message, portID) => { 11 | const { tabId, action } = message; 12 | // Initial connection – store current instance of DevTools page 13 | if (action === 'connectChromogen') { 14 | connections[tabId] = portID; 15 | } 16 | // Relay message to content.ts -> package 17 | chrome.tabs.sendMessage(Number(tabId), message); 18 | }; 19 | 20 | // Add event listener defined above to current DevTools panel instance 21 | port.onMessage.addListener(extensionListener); 22 | 23 | // Handle disconnect 24 | port.onDisconnect.addListener((portID) => { 25 | portID.onMessage.removeListener(extensionListener); 26 | // remove current DevTool instance from connections 27 | // eslint-disable-next-line no-restricted-syntax 28 | for (const key in connections) { 29 | if (connections[key] === portID) { 30 | delete connections[key]; 31 | break; 32 | } 33 | } 34 | }); 35 | }); 36 | 37 | // Listen for messages from Chromogen package (sent via content.js) 38 | chrome.runtime.onMessage.addListener((message: Message, sender) => { 39 | const { tab } = sender; 40 | 41 | if (tab) { 42 | const tabId = `${tab.id}`; 43 | // Relay message to devTool instance 44 | if (connections[tabId]) { 45 | connections[tabId].postMessage({ 46 | action: message.action, 47 | }); 48 | } 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen-root", 3 | "version": "1.0.0", 4 | "description": "simple, interaction-driven test generator for Recoil apps", 5 | "scripts": { 6 | "ci-all": "(npm ci); (cd ./package && npm ci); (cd ./dev-tool && npm ci); (cd ./demo-todo && npm ci);", 7 | "build": "echo \"ERROR! Nothing to build in root directory.\n\"" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/oslabs-beta/Chromogen.git" 12 | }, 13 | "contributors": [ 14 | { 15 | "name": "Michelle Holland", 16 | "url": "https://github.com/michellebholland/" 17 | }, 18 | { 19 | "name": "Jim Chen", 20 | "url": "https://github.com/chenchingk" 21 | }, 22 | { 23 | "name": "Andy Wang", 24 | "url": "https://github.com/andywang23" 25 | }, 26 | { 27 | "name": "Connor Rose Delisle", 28 | "url": "https://github.com/connorrose" 29 | } 30 | ], 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/oslabs-beta/Chromogen/issues" 34 | }, 35 | "homepage": "https://github.com/oslabs-beta/Chromogen#readme", 36 | "devDependencies": { 37 | "@testing-library/react-hooks": "^3.4.2", 38 | "@typescript-eslint/parser": "^3.9.1", 39 | "eslint": "^7.2.0", 40 | "eslint-config-airbnb": "^18.2.0", 41 | "eslint-config-prettier": "^6.11.0", 42 | "eslint-plugin-import": "^2.22.0", 43 | "eslint-plugin-jsx-a11y": "^6.3.1", 44 | "eslint-plugin-prettier": "^3.1.4", 45 | "eslint-plugin-react": "^7.20.5", 46 | "eslint-plugin-react-hooks": "^4.0.0", 47 | "prettier": "npm:@btmills/prettier@^2.1.1", 48 | "typescript": "^4.0.3" 49 | }, 50 | "dependencies": { 51 | "@types/jest": "^26.0.14", 52 | "redux": "^4.0.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /dev-tool/app/Components/App.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React, { useState, useEffect } from 'react'; 3 | import GitHubIcon from '@material-ui/icons/GitHub'; 4 | import Recorder from './Recorder'; 5 | /* eslint-enable */ 6 | 7 | const App: React.FC = () => { 8 | const [status, setStatus] = useState(true); 9 | const [connected, setConnected] = useState(false); 10 | 11 | useEffect(() => { 12 | // Create a connection to the background page 13 | const backgroundConnection = chrome.runtime.connect(); 14 | // Send tab ID to background.js 15 | backgroundConnection.postMessage({ 16 | action: 'connectChromogen', 17 | tabId: chrome.devtools.inspectedWindow.tabId, 18 | }); 19 | // Listen for messages from background.js 20 | backgroundConnection.onMessage.addListener((message) => { 21 | if (message.action === 'moduleConnected') { 22 | setConnected(true); 23 | } 24 | if (message.action === 'setStatus') { 25 | setStatus(!status); 26 | } 27 | }); 28 | }, [connected, status]); 29 | 30 | return connected ? ( 31 | // Render extension if Chromogen is installed 32 |
33 |
chromogen
34 | 35 |
36 | ) : ( 37 | // Otherwise, render 'please install' message along with Github Icon 38 |
39 |
40 | 41 |
42 |
Please
43 | npm install chromogen 44 |
in your app before using this extension.
45 |
46 | github.com/oslabs-beta/Chromogen 47 |
48 |
49 | 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /dev-tool/build/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* General styles */ 2 | html { 3 | height: 100vh; 4 | color: #d4d4d8; 5 | background-color: rgb(30, 30, 31); 6 | } 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | /* App.jsx */ 13 | .App { 14 | display: grid; 15 | grid-template-rows: 33fr 33fr 33fr; 16 | font-family: 'Palanquin', sans-serif; 17 | height: 100vh; 18 | } 19 | .App div { 20 | text-align: center; 21 | font-size: 2.5rem; 22 | letter-spacing: 4px; 23 | width: 100%; 24 | text-shadow: #83beb1c2 1px 2px 0; 25 | } 26 | 27 | /* Recorder.jsx */ 28 | .recorder-div { 29 | display: flex; 30 | flex-direction: row; 31 | justify-content: space-evenly; 32 | align-items: center; 33 | border-top: 2px solid #83beb1; 34 | border-bottom: 2px solid #83beb1; 35 | background-color: rgb(48, 48, 49); 36 | } 37 | #recorderBtn { 38 | border: none; 39 | box-shadow: 0px 0px 9px 0px rgba(165, 165, 165, 0.123); 40 | border-radius: 11px; 41 | background-color: rgb(30, 30, 31); 42 | color: #8ccabd; 43 | width: 200px; 44 | height: 50px; 45 | font-size: 18px; 46 | } 47 | #recorderBtn:focus { 48 | outline: none; 49 | color: #5f817a98; 50 | } 51 | #recorderBtn:hover { 52 | border: 2px solid rgb(48, 48, 49); 53 | } 54 | 55 | /* Install Chromogen render */ 56 | #installContainer { 57 | display: grid; 58 | grid-template-rows: 33fr 33fr 33fr; 59 | height: 100vh; 60 | text-align: center; 61 | } 62 | #installMessage div, code, span { 63 | height: 40px; 64 | margin-top: 15px; 65 | font-size: 1.7rem; 66 | text-decoration: none; 67 | vertical-align: middle; 68 | } 69 | code { 70 | color: #8ccabd; 71 | } 72 | span { 73 | font-size: 1rem; 74 | margin-top: 5px; 75 | } 76 | 77 | /* Media queries */ 78 | 79 | @media (max-width: 450px) { 80 | .recorder-div { 81 | flex-direction: column; 82 | } 83 | } -------------------------------------------------------------------------------- /dev-tool/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen-devtool", 3 | "version": "0.1.0", 4 | "description": "DevTool panel for use with Chromogen testing package", 5 | "scripts": { 6 | "build": "webpack --mode=production", 7 | "dev": "webpack --mode=development --watch" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/open-source-labs/Chromogen.git" 12 | }, 13 | "contributors": [ 14 | { 15 | "name": "Jim Chen", 16 | "url": "https://github.com/chenchingk" 17 | }, 18 | { 19 | "name": "Andy Wang", 20 | "url": "https://github.com/andywang23" 21 | }, 22 | { 23 | "name": "Connor Rose Delisle", 24 | "url": "https://github.com/connorrose" 25 | }, 26 | { 27 | "name": "Michelle Holland", 28 | "url": "https://github.com/michellebholland/" 29 | } 30 | ], 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/open-source-labs/Chromogen/issues" 34 | }, 35 | "homepage": "https://github.com/open-source-labs/Chromogen#readme", 36 | "keywords": [ 37 | "automated", 38 | "test", 39 | "generator", 40 | "recoil", 41 | "react", 42 | "chrome", 43 | "devtools", 44 | "extension", 45 | "interactive" 46 | ], 47 | "dependencies": { 48 | "@material-ui/core": "^4.11.0", 49 | "@material-ui/icons": "^4.9.1", 50 | "@types/chrome": "0.0.122", 51 | "@types/react": "^16.9.46", 52 | "@types/react-dom": "^16.9.8", 53 | "react": "^16.13.1", 54 | "react-dom": "^16.13.1", 55 | "recoil": "0.0.10" 56 | }, 57 | "devDependencies": { 58 | "css-loader": "^4.2.1", 59 | "source-map-loader": "^1.0.1", 60 | "style-loader": "^1.2.1", 61 | "ts-loader": "^8.0.3", 62 | "typescript": "^4.0.3", 63 | "webpack": "^4.44.1", 64 | "webpack-chrome-extension-reloader": "^1.3.0", 65 | "webpack-cli": "^3.3.12" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import Checkbox from '@material-ui/core/Checkbox'; 4 | import { todoListState } from '../store/atoms'; 5 | import '../styles/styles.css'; 6 | 7 | function replaceItemAtIndex(arr, index, newValue) { 8 | return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; 9 | } 10 | 11 | function removeItemAtIndex(arr, index) { 12 | return [...arr.slice(0, index), ...arr.slice(index + 1)]; 13 | } 14 | 15 | const TodoItem = ({ item }) => { 16 | const [todoList, setTodoList] = useRecoilState(todoListState); 17 | const index = todoList.findIndex((listItem) => listItem === item); 18 | 19 | const editItemText = ({ target: { value } }) => { 20 | const newList = replaceItemAtIndex(todoList, index, { 21 | ...item, 22 | text: value, 23 | }); 24 | setTodoList(newList); 25 | }; 26 | const toggleItemCompletion = () => { 27 | const newList = replaceItemAtIndex(todoList, index, { 28 | ...item, 29 | isComplete: !item.isComplete, 30 | }); 31 | setTodoList(newList); 32 | }; 33 | const deleteItem = () => { 34 | const newList = removeItemAtIndex(todoList, index); 35 | setTodoList(newList); 36 | }; 37 | 38 | const checkBoxClasses = { 39 | low: 'lowPriority', 40 | medium: 'mediumPriority', 41 | high: 'highPriority', 42 | }; 43 | 44 | return ( 45 |
46 | 47 | 54 | 57 |
58 | ); 59 | }; 60 | export default TodoItem; 61 | -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen", 3 | "version": "1.3.4", 4 | "description": "simple, interaction-driven Jest test generator for Recoil apps", 5 | "main": "build/index.js", 6 | "keywords": [ 7 | "react", 8 | "recoil", 9 | "jest", 10 | "testing" 11 | ], 12 | "files": [ 13 | "build" 14 | ], 15 | "scripts": { 16 | "prepublishOnly": "npm run build", 17 | "build": "tsc", 18 | "test": "jest --verbose --coverage", 19 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/open-source-labs/Chromogen.git" 24 | }, 25 | "contributors": [ 26 | { 27 | "name": "Michelle Holland", 28 | "url": "https://github.com/michellebholland/" 29 | }, 30 | { 31 | "name": "Jim Chen", 32 | "url": "https://github.com/chenchingk" 33 | }, 34 | { 35 | "name": "Andy Wang", 36 | "url": "https://github.com/andywang23" 37 | }, 38 | { 39 | "name": "Connor Rose Delisle", 40 | "url": "https://github.com/connorrose" 41 | } 42 | ], 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/open-source-labs/Chromogen/issues" 46 | }, 47 | "homepage": "https://github.com/open-source-labs/Chromogen#readme", 48 | "peerDependencies": { 49 | "jest": ">=24.0.0", 50 | "typescript": ">=3.8.0" 51 | }, 52 | "dependencies": { 53 | "react": "^16.13.1", 54 | "react-dom": "^16.13.1", 55 | "react-recoil-hooks-testing-library": "0.0.8", 56 | "recoil": "0.0.13", 57 | "redux": "^4.0.5" 58 | }, 59 | "devDependencies": { 60 | "@babel/core": "^7.11.6", 61 | "@babel/preset-env": "^7.11.5", 62 | "@babel/preset-react": "^7.10.4", 63 | "@babel/preset-typescript": "^7.10.4", 64 | "@testing-library/react": "^11.0.4", 65 | "@testing-library/react-hooks": "^3.4.2", 66 | "@types/node": "^14.11.2", 67 | "@types/react": "^16.9.49", 68 | "babel-jest": "^26.3.0", 69 | "coveralls": "^3.1.0", 70 | "jest": "^26.4.2", 71 | "react-test-renderer": "^16.13.1", 72 | "typescript": "^4.0.3" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /demo-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen-todo", 3 | "version": "1.0.0", 4 | "description": "demo todo app for Chromogen using React + Recoil", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --open", 8 | "test": "jest --verbose" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "recoil", 13 | "chromogen", 14 | "demo", 15 | "example", 16 | "todo" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/open-source-labs/Chromogen.git" 21 | }, 22 | "contributors": [ 23 | { 24 | "name": "Michelle Holland", 25 | "url": "https://github.com/michellebholland/" 26 | }, 27 | { 28 | "name": "Jim Chen", 29 | "url": "https://github.com/chenchingk" 30 | }, 31 | { 32 | "name": "Andy Wang", 33 | "url": "https://github.com/andywang23" 34 | }, 35 | { 36 | "name": "Connor Rose Delisle", 37 | "url": "https://github.com/connorrose" 38 | } 39 | ], 40 | "license": "MIT", 41 | "devDependencies": { 42 | "@babel/core": "^7.11.1", 43 | "@babel/plugin-transform-runtime": "^7.11.0", 44 | "@babel/preset-env": "^7.11.0", 45 | "@babel/preset-react": "^7.10.4", 46 | "@testing-library/react-hooks": "^3.4.1", 47 | "babel-loader": "^8.1.0", 48 | "css-loader": "^4.2.1", 49 | "identity-obj-proxy": "^3.0.0", 50 | "jest": "^26.4.2", 51 | "react-hot-loader": "^4.12.21", 52 | "react-recoil-hooks-testing-library": "0.0.8", 53 | "react-test-renderer": "^16.13.1", 54 | "style-loader": "^1.2.1", 55 | "webpack": "^4.44.1", 56 | "webpack-cli": "^3.3.12", 57 | "webpack-dev-server": "^3.11.0" 58 | }, 59 | "peerDependencies": { 60 | "typescript": "^4.0.3" 61 | }, 62 | "dependencies": { 63 | "@babel/runtime": "^7.11.2", 64 | "@material-ui/core": "^4.11.0", 65 | "@material-ui/icons": "^4.9.1", 66 | "babel-jest": "^26.3.0", 67 | "chromogen": "^1.3.4", 68 | "react": "^16.13.1", 69 | "react-dom": "^16.13.1", 70 | "recoil": "0.0.10", 71 | "typescript": "^4.0.3" 72 | }, 73 | "jest": { 74 | "moduleNameMapper": { 75 | "\\.(css|less)$": "identity-obj-proxy" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 5 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 6 | "declaration": true /* Generates corresponding '.d.ts' file. */, 7 | "outDir": "./build" /* Redirect output structure to the directory. */, 8 | "rootDir": "." /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 9 | "removeComments": true /* Do not emit comments to output. */, 10 | "strict": true /* Enable all strict type-checking options. */, 11 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 12 | "strictNullChecks": true /* Enable strict null checks. */, 13 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 14 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 15 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 16 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 17 | "noUnusedLocals": true /* Report errors on unused locals. */, 18 | "noUnusedParameters": true /* Report errors on unused parameters. */, 19 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 20 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 21 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 22 | "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, 23 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 24 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "browser": true, 6 | "es2020": true, 7 | "jest": true 8 | }, 9 | "globals": { 10 | "chrome": "readonly" 11 | }, 12 | "extends": [ 13 | "airbnb", 14 | "prettier", 15 | "prettier/react" 16 | ], 17 | "plugins": ["prettier"], 18 | "parserOptions": { 19 | "ecmaVersion": 2020, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true 23 | } 24 | }, 25 | "rules": { 26 | "prettier/prettier": ["warn"], 27 | "arrow-body-style": ["error", "as-needed"], 28 | "default-case-last": "error", 29 | "default-param-last": ["error"], 30 | "func-style": ["off", "expression"], 31 | "no-constant-condition": "error", 32 | "no-useless-call": "error", 33 | "prefer-exponentiation-operator": "error", 34 | "prefer-regex-literals": "error", 35 | "quotes": [ 36 | "error", 37 | "single", 38 | { 39 | "avoidEscape": true, 40 | "allowTemplateLiterals": false 41 | } 42 | ], 43 | "import/prefer-default-export": "off", 44 | "react/jsx-filename-extension": ["off"], 45 | "react/function-component-definition": [ 46 | "error", 47 | { 48 | "namedComponents": "arrow-function", 49 | "unnamedComponents": "arrow-function" 50 | } 51 | ], 52 | "react/jsx-handler-names": [ 53 | "error", 54 | { 55 | "eventHandlerPrefix": "handle", 56 | "eventHandlerPropPrefix": "on" 57 | } 58 | ], 59 | "react/jsx-key": "error", 60 | "react/jsx-no-useless-fragment": "error", 61 | "react/jsx-sort-props": [ 62 | "error", 63 | { 64 | "callbacksLast": true, 65 | "shorthandFirst": true, 66 | "shorthandLast": false, 67 | "ignoreCase": true, 68 | "noSortAlphabetically": false, 69 | "reservedFirst": true 70 | } 71 | ], 72 | "react/no-adjacent-inline-elements": "error", 73 | "react/no-direct-mutation-state": "error", 74 | "react/no-multi-comp": "error", 75 | "react/prop-types": [ 76 | "error", 77 | { "skipUndeclared": true } 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /package/hooks_generator/hooks_src/api/hooks-core-utils.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, useMemo, Dispatch, useState, useEffect } from 'react'; 2 | import { hooksLedger } from '../utils/hooks-ledger'; 3 | import { EnhancedStore } from '../utils/hooks-store'; 4 | 5 | export function useHookedReducer( 6 | reducer: Reducer, 7 | initialState: S, 8 | store: EnhancedStore, 9 | reducerId: string | number, 10 | ): [S, Dispatch] { 11 | const initialReducerState = useMemo(() => { 12 | const initialStateInStore = store.getState()[reducerId]; 13 | return initialStateInStore === undefined ? initialState : initialStateInStore; 14 | }, []); 15 | 16 | const [localState, setState] = useState(initialReducerState); 17 | // Creating state property in store to save all state changes 18 | store.subscribe(() => (hooksLedger.state = store.getState()[reducerId])); 19 | // store.subscribe(() => console.log(store.getState())); 20 | 21 | const dispatch = useMemo>(() => { 22 | const dispatch = (action: any) => { 23 | if (action && typeof action === 'object' && typeof action.type === 'string') { 24 | store.dispatch({ 25 | type: `${reducerId}/${action.type}`, 26 | payload: action, 27 | }); 28 | } else { 29 | store.subscribe(() => { 30 | hooksLedger.state = store.getState()[reducerId]; 31 | hooksLedger.id = reducerId; 32 | hooksLedger.initialState = hooksLedger.state[0]; 33 | }); 34 | 35 | 36 | store.subscribe(() => hooksLedger.currState = hooksLedger.state[length - 1]); 37 | 38 | store.subscribe(() => hooksLedger.dispCount = hooksLedger.dispCount + 1); 39 | 40 | store.dispatch({ 41 | type: reducerId, 42 | payload: action, 43 | }); 44 | } 45 | }; 46 | 47 | return dispatch; 48 | }, []); 49 | 50 | useEffect(() => { 51 | const teardown = store.registerHookedReducer(reducer, initialReducerState, reducerId); 52 | 53 | let lastReducerState = localState; 54 | const unsubscribe = store.subscribe(() => { 55 | const storeState: any = store.getState(); 56 | const reducerState = storeState[reducerId]; 57 | 58 | if (lastReducerState !== reducerState) { 59 | setState(reducerState); 60 | } 61 | 62 | lastReducerState = reducerState; 63 | }); 64 | 65 | return () => { 66 | unsubscribe(); 67 | teardown(); 68 | }; 69 | }, []); 70 | 71 | // Returns a tuple 72 | return [localState, dispatch]; 73 | } 74 | -------------------------------------------------------------------------------- /package/recoil_generator/src/api/family-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { SerializableParam } from 'recoil'; 3 | 4 | import { ledger } from '../utils/ledger'; 5 | import { dummyParam } from '../utils/utils'; 6 | import { recordingState } from '../utils/store'; 7 | import { debouncedAddToTransactions } from './core-utils'; 8 | /* eslint-enable */ 9 | 10 | const { transactions, selectorFamilies, initialRenderFamilies, setTransactions } = ledger; 11 | 12 | export const wrapFamilyGetter = (key: string, configGet: Function) => { 13 | let returnedPromise = false; 14 | 15 | return (params: SerializableParam) => (utils: any) => { 16 | const { get } = utils; 17 | const value = configGet(params)(utils); 18 | // Only capture selector data if currently recording 19 | 20 | if (get(recordingState)) { 21 | if (transactions.length === 0) { 22 | // Promise-validation is expensive, so we only do it once, on initial load 23 | if ( 24 | typeof value === 'object' 25 | && value !== null 26 | && Object.prototype.toString.call(value) === '[object Promise]' 27 | ) { 28 | delete selectorFamilies[key]; 29 | returnedPromise = true; 30 | } else { 31 | initialRenderFamilies.push({ key, params, value }); 32 | } 33 | } else if (!returnedPromise) { 34 | // Track every new params 35 | if (!selectorFamilies[key].prevParams.has(params)) { 36 | selectorFamilies[key].prevParams.add(params); 37 | } 38 | // Debouncing allows TransactionObserver to push to array first 39 | // Length must be computed within debounce to correctly find last transaction 40 | // Excluding dummy selector created by ChromogenObserver's onload useEffect hook 41 | if (params !== dummyParam) debouncedAddToTransactions(key, value, params); 42 | } 43 | } 44 | 45 | // Return value from original get method 46 | return value; 47 | }; 48 | }; 49 | 50 | export const wrapFamilySetter = (key: string, set: Function) => (params: SerializableParam) => ( 51 | utils: any, 52 | newValue: any, 53 | ) => { 54 | if (utils.get(recordingState) && setTransactions.length > 0) { 55 | // allow TransactionObserver to push to array first 56 | // Length must be computed after timeout to correctly find last transaction 57 | setTimeout(() => { 58 | setTransactions[setTransactions.length - 1].setter = { key, params, newValue }; 59 | }, 0); 60 | } 61 | return set(params)(utils, newValue); 62 | }; 63 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | import { debounce, convertFamilyTrackerKeys } from '../src/utils/utils.ts'; 2 | 3 | jest.useFakeTimers(); 4 | 5 | describe('debounce', () => { 6 | it('should return a new function', () => { 7 | // declare mock function that returns a string 8 | const inputFunction = () => 'example'; 9 | // declare a function that stores the evaluated result of invoking debounce on inputFunction with a wait time of 0s 10 | const outputFunction = debounce(inputFunction, 0); 11 | // verify that outputFunction is a function 12 | expect(typeof outputFunction).toBe('function'); 13 | // verify that that the 'debounced function' (outputFunction) is different than the parameter function (inputFunction) 14 | expect(outputFunction).not.toBe(inputFunction); 15 | }); 16 | 17 | it('should limit consecutive calls', () => { 18 | // increment count to 1 after 100ms 19 | let count = 0; 20 | const increment = debounce(() => { 21 | count += 1; 22 | }, 100); 23 | // invoke increment twice 24 | increment(); 25 | increment(); 26 | // advance timer to 101ms using a mock jest function 27 | jest.advanceTimersByTime(101); 28 | // verify that count only incremented once because it was debounced 29 | expect(count).toEqual(1); 30 | }); 31 | }); 32 | 33 | // testing convertFamilyTrackerKeys 34 | describe('convertFamilyTrackerKeys', () => { 35 | it('should update key names if in map', () => { 36 | // create mock tracker (object) 37 | const newTracker = convertFamilyTrackerKeys( 38 | // first parameter is an object with a property whose value is a string 39 | { keyOne: 'some value' }, 40 | // second parameter is a new Map 41 | new Map([['keyOne', 'keyUpdated']]), 42 | ); 43 | // verify that newTracker object includes property from Map (keyUpdated) 44 | expect(newTracker).toHaveProperty('keyUpdated'); 45 | // verify that newTracker did not update key name with first parameter 46 | expect(newTracker).not.toHaveProperty('keyOne'); 47 | }); 48 | 49 | it('should preserve key names if not in map', () => { 50 | // create mock tracker (object) 51 | const newTracker = convertFamilyTrackerKeys( 52 | { keyOne: 'some value' }, 53 | new Map([['keyTwo', 'keyNotUpdated']]), 54 | ); 55 | // verify that newTracker's first parameter has been preserved 56 | expect(newTracker).toHaveProperty('keyOne'); 57 | // verify that newTracker does have keyTwo in first parameter 58 | expect(newTracker).not.toHaveProperty('keyTwo'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /package/recoil_generator/src/output/output.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Ledger } from '../types'; 3 | import type { SerializableParam } from 'recoil'; 4 | import { 5 | importRecoilState, 6 | writeableHook, 7 | readableHook, 8 | returnWriteable, 9 | returnReadable, 10 | testSelectors, 11 | testSetters, 12 | importRecoilFamily, 13 | atomFamilyHook, 14 | selectorFamilyHook, 15 | returnSelectorFamily, 16 | initializeSelectors, 17 | returnAtomFamily, 18 | } from './output-utils'; 19 | 20 | /* eslint-enable */ 21 | 22 | /* ----- HELPERS ----- */ 23 | export const setFilter = (selectors: string[], setters: string[]): string[] => 24 | selectors.filter((key) => !setters.includes(key)); 25 | 26 | /* ----- MAIN ----- */ 27 | export const output = ({ 28 | atoms, 29 | selectors, 30 | setters, 31 | atomFamilies, 32 | selectorFamilies, 33 | initialRender, 34 | transactions, 35 | setTransactions, 36 | }: Ledger): string => 37 | `import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library'; 38 | import { useRecoilValue, useRecoilState } from 'recoil'; 39 | import { 40 | ${ 41 | importRecoilState(atoms) 42 | + importRecoilState(selectors) 43 | + importRecoilFamily(atomFamilies) 44 | + importRecoilFamily(selectorFamilies) 45 | } 46 | } from ''; 47 | 48 | // Suppress 'Batcher' warnings from React / Recoil conflict 49 | console.error = jest.fn(); 50 | 51 | // Hook to return atom/selector values and/or modifiers for react-recoil-hooks-testing-library 52 | const useStoreHook = () => { 53 | // atoms 54 | ${writeableHook(atoms)} 55 | // writeable selectors 56 | ${writeableHook(setters)} 57 | // read-only selectors 58 | ${readableHook(setFilter(selectors, setters))} 59 | // atom families 60 | ${atomFamilyHook(transactions)} 61 | // writeable selector families 62 | ${selectorFamilyHook(selectorFamilies, true)} 63 | // read-only selector families 64 | ${selectorFamilyHook(selectorFamilies, false)} 65 | 66 | 67 | 68 | return { 69 | ${ 70 | returnWriteable(atoms) 71 | + returnWriteable(setters) 72 | + returnReadable(setFilter(selectors, setters)) 73 | + returnAtomFamily(transactions) 74 | + returnSelectorFamily(selectorFamilies, true) 75 | + returnSelectorFamily(selectorFamilies, false) 76 | }\t}; 77 | }; 78 | 79 | describe('INITIAL RENDER', () => { 80 | const { result } = renderRecoilHook(useStoreHook); 81 | 82 | ${initializeSelectors(initialRender)} 83 | }); 84 | 85 | describe('SELECTORS', () => { 86 | ${testSelectors(transactions)}}); 87 | 88 | describe('SETTERS', () => { 89 | ${testSetters(setTransactions)}});`; 90 | -------------------------------------------------------------------------------- /package/recoil_generator/src/api/core-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { debounce } from '../utils/utils'; 3 | import { ledger } from '../utils/ledger'; 4 | import { recordingState } from '../utils/store'; 5 | /* eslint-enable */ 6 | 7 | const { transactions, initialRender, selectors, setTransactions } = ledger; 8 | 9 | const DEBOUNCE_MS = 250; 10 | 11 | // Set timeout for selector get calls 12 | export const debouncedAddToTransactions = debounce( 13 | (key, value, params) => 14 | params !== undefined 15 | ? transactions[transactions.length - 1].familyUpdates.push({ key, value, params }) 16 | : transactions[transactions.length - 1].updates.push({ key, value }), 17 | DEBOUNCE_MS, 18 | ); 19 | 20 | // the logic for recording selectors only when they fire 21 | // whenever get method is fired, chromogen records 22 | export const wrapGetter = (key: string, get: Function) => { 23 | let returnedPromise: boolean = false; 24 | 25 | return (utils: any) => { 26 | //will return what normal recoil selector will return aka regular selector method 27 | const value = get(utils); 28 | 29 | //Checking whether value is async 30 | // Only capture selector data if currently recording (if record button has been hit) 31 | if (utils.get(recordingState)) { 32 | //making sure no transactions have been fired 33 | if (transactions.length === 0) { 34 | // Promise-validation is expensive, so we only do it once, on initial load 35 | if (typeof value === 'object' && value !== null && value.constructor.name === 'Promise') { 36 | ledger.selectors = selectors.filter((current) => current !== key); 37 | returnedPromise = true; 38 | } else { 39 | initialRender.push({ key, value }); 40 | } 41 | } else if (!returnedPromise) { 42 | // Debouncing (throttling) allows TransactionObserver to push to array first 43 | // Length must be computed within debounce to correctly find last transaction 44 | // only capture meaningful function calls 45 | // when called, timer starts; if x amount of time passes and function isnt called again, it fires; if called, resets timer 46 | debouncedAddToTransactions(key, value); 47 | } 48 | } 49 | 50 | return value; 51 | }; 52 | }; 53 | 54 | export const wrapSetter = (key: string, set: Function) => (utils: any, newValue: any) => { 55 | if (utils.get(recordingState) && setTransactions.length > 0) { 56 | // allow TransactionObserver to push to array first 57 | // Length must be computed after timeout to correctly find last transaction 58 | // this is here b/c of async stuff with useRecoilTransactionObserver 59 | setTimeout(() => { 60 | setTransactions[setTransactions.length - 1].setter = { key, newValue }; 61 | }, 0); 62 | } 63 | // returns what regular selector would return (?) 64 | return set(utils, newValue); 65 | }; 66 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/api.test.js: -------------------------------------------------------------------------------- 1 | import { ledger } from '../src/utils/ledger.ts'; 2 | import { atom, selector, selectorFamily, atomFamily } from '../src/api/api.ts'; 3 | 4 | // testing the atom 5 | describe('atom', () => { 6 | // destructuring atoms from ledger interface in utils folder 7 | const { atoms } = ledger; 8 | it('is a function', () => { 9 | expect(typeof atom).toBe('function'); 10 | }); 11 | 12 | it('should update ledger upon invocation', () => { 13 | // creating a mock atom 14 | atom({ 15 | key: 'exampleAtom', 16 | default: false, 17 | }); 18 | // verifying atoms property (array) on ledger has been updated with input atom 19 | expect(atoms).toHaveLength(1); 20 | }); 21 | 22 | it('should create Recoil atom with correct key name', () => { 23 | // verifying that input atom key matches 'exampleAtom' 24 | expect(atoms[0]).toHaveProperty('key', 'exampleAtom'); 25 | }); 26 | }); 27 | 28 | describe('selector', () => { 29 | // destructuring selectors from ledger object in utils folder 30 | const { selectors } = ledger; 31 | const test = true; 32 | 33 | it('is a function', () => { 34 | // verify selector is a function 35 | expect(typeof selector).toBe('function'); 36 | }); 37 | 38 | it('should update ledger upon invocation', () => { 39 | // creating a mock selector with key, get, set 40 | selector({ 41 | key: 'exampleSelector', 42 | get: () => 'getMethod', 43 | set: () => 'setMethod', 44 | }); 45 | // verify selectors property in ledger has been updated with mock selector 46 | expect(selectors).toHaveLength(1); 47 | }); 48 | // verifying that input selector key matches 'exampleSelector' 49 | it('should capture correct key name', () => { 50 | expect(selectors[0]).toEqual('exampleSelector'); 51 | }); 52 | 53 | it('should return an object if an input condition evaluates to true', () => { 54 | // verify that selector (recoilSelector in this context) invocation returns an object 55 | expect(typeof selector(test)).toBe('object'); 56 | }); 57 | }); 58 | 59 | describe('atomFamily', () => { 60 | it('should return a function', () => { 61 | // create a mock atomFamily 62 | const familyFactory = atomFamily({ 63 | key: 'familyKey', 64 | default: (param) => param.toString(), 65 | }); 66 | // verify that familyFactory is a function 67 | expect(typeof familyFactory).toEqual('function'); 68 | }); 69 | }); 70 | 71 | describe('selectorFamily', () => { 72 | // truthy parameter 73 | const test = true; 74 | it('should return a function', () => { 75 | // create a mock selectorFamily 76 | const familyFactory = selectorFamily({ 77 | key: 'familyKey', 78 | get: () => () => 'some value', 79 | set: () => () => undefined, 80 | default: (param) => param.toString(), 81 | }); 82 | // verify that familyFactory is a function 83 | expect(typeof familyFactory).toEqual('function'); 84 | }); 85 | it('should return an object if an input condition evaluates to true', () => { 86 | // verify that selectorFamily (recoilSelectorFamily in this context) invocation returns an object 87 | expect(typeof selectorFamily(test)).toBe('function'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /package/recoil_generator/src/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { 3 | RecoilState, 4 | RecoilValue, 5 | DefaultValue, 6 | SerializableParam, 7 | RecoilValueReadOnly, 8 | } from 'recoil'; 9 | /* eslint-enable */ 10 | 11 | // ----- INITIALIZING NON-IMPORTABLE RECOIL TYPES ----- 12 | type ResetRecoilState = (recoilVal: RecoilState) => void; 13 | 14 | type GetRecoilValue = (recoilVal: RecoilValue) => T; 15 | 16 | type SetRecoilState = ( 17 | recoilVal: RecoilState, 18 | newVal: T | DefaultValue | ((prevValue: T) => T | DefaultValue), 19 | ) => void; 20 | 21 | // ----- EXPORTING TYPES TO BE USED IN SRC/.TSX FILES ----- 22 | export interface SetterUpdate { 23 | key: string; 24 | newValue: any; 25 | params?: SerializableParam; 26 | } 27 | 28 | export interface SelectorUpdate { 29 | key: string; 30 | value: any; 31 | } 32 | 33 | export interface SelectorFamilyUpdate extends SelectorUpdate { 34 | params: SerializableParam; 35 | } 36 | 37 | export interface AtomUpdate extends SelectorUpdate { 38 | previous: any; 39 | updated: boolean; 40 | } 41 | 42 | export interface AtomFamilyState { 43 | family: string; 44 | key: string; 45 | value: any; 46 | updated: boolean; 47 | } 48 | 49 | export interface Transaction { 50 | state: AtomUpdate[]; 51 | updates: SelectorUpdate[]; 52 | atomFamilyState: AtomFamilyState[]; 53 | familyUpdates: SelectorFamilyUpdate[]; 54 | } 55 | 56 | export interface SetTransaction { 57 | state: AtomUpdate[]; 58 | setter: null | SetterUpdate; 59 | } 60 | 61 | export interface AtomFamilyMembers { 62 | [atomName: string]: RecoilState; 63 | } 64 | export interface AtomFamilies { 65 | [familyName: string]: AtomFamilyMembers; 66 | } 67 | 68 | export interface SelectorFamilyConfig { 69 | key: string; 70 | get: (param: P) => (opts: { get: GetRecoilValue }) => Promise | RecoilValue | T; 71 | set?: ( 72 | param: P, 73 | ) => ( 74 | opts: { set: SetRecoilState; get: GetRecoilValue; reset: ResetRecoilState }, 75 | newValue: T | DefaultValue, 76 | ) => void; 77 | dangerouslyAllowMutability?: boolean; 78 | } 79 | export interface SelectorFamilyMembers { 80 | trackedSelectorFamily: (param: P) => RecoilState | RecoilValueReadOnly; 81 | isSettable: boolean; 82 | prevParams: Set; 83 | } 84 | export interface SelectorFamilies { 85 | [familyName: string]: SelectorFamilyMembers; 86 | } 87 | 88 | // atoms should take RecoilState[] | string[] 89 | export interface Ledger { 90 | atoms: T[]; 91 | selectors: string[]; 92 | atomFamilies: AtomFamilies; 93 | selectorFamilies: SelectorFamilies; 94 | setters: string[]; 95 | initialRender: SelectorUpdate[]; 96 | initialRenderFamilies: SelectorFamilyUpdate[]; 97 | transactions: Transaction[]; 98 | setTransactions: SetTransaction[]; 99 | } 100 | 101 | export interface SelectorConfig { 102 | key: string; 103 | get: (opts: { get: GetRecoilValue }) => T | Promise | RecoilValue; 104 | set?: ( 105 | opts: { get: GetRecoilValue; set: SetRecoilState; reset: ResetRecoilState }, 106 | newValue: T | DefaultValue, 107 | ) => void; 108 | dangerouslyAllowMutability?: boolean; 109 | } 110 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoListFilters.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import SortIcon from '@material-ui/icons/Sort'; 3 | import EqualizerIcon from '@material-ui/icons/Equalizer'; 4 | import RefreshIcon from '@material-ui/icons/Refresh'; 5 | import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'; 6 | import { todoListStatsState, todoListSortedStats, refreshFilterState } from '../store/store'; 7 | import { todoListFilterState, todoListSortState } from '../store/atoms'; 8 | 9 | const TodoListFilters = () => { 10 | const [filter, setFilter] = useRecoilState(todoListFilterState); 11 | // selector - grabs totals for each category 12 | const { high, medium, low } = useRecoilValue(todoListSortedStats); 13 | // selector *writeable - resets sort and filter 14 | const resetFilters = useResetRecoilState(refreshFilterState); 15 | // selector - toggles sort on and off 16 | const [sort, setSort] = useRecoilState(todoListSortState); 17 | // toggle priority stats display 18 | const [displayStats, setDisplayStats] = useState(false); 19 | // selector - totals for each filter 20 | const { totalNum, totalCompletedNum, totalUncompletedNum } = useRecoilValue(todoListStatsState); 21 | const updateFilter = ({ target: { value } }) => setFilter(value); 22 | 23 | const toggleSort = () => setSort(!sort); 24 | const toggleDisplayStats = () => setDisplayStats(!displayStats); 25 | const reset = () => { 26 | setDisplayStats(false); // displayStats is local state 27 | resetFilters(); 28 | }; 29 | 30 | const sortIconColor = { 31 | true: 'sortedWhite', 32 | false: 'unsortedGray', 33 | }; 34 | 35 | return ( 36 |
    37 | 47 | 57 | 66 | 69 | 70 | 81 | 84 |
85 | ); 86 | }; 87 | 88 | export default TodoListFilters; 89 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoItemCreator.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React, { useState } from 'react'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import { green, yellow, red } from '@material-ui/core/colors'; 5 | import RadioGroup from '@material-ui/core/RadioGroup'; 6 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import FormLabel from '@material-ui/core/FormLabel'; 9 | import Radio from '@material-ui/core/Radio'; 10 | import { useSetRecoilState } from 'recoil'; 11 | import { todoListState } from '../store/atoms'; 12 | 13 | // utility for creating unique Id 14 | let id = 0; 15 | const getId = () => { 16 | id += 1; 17 | return id; 18 | }; 19 | 20 | const TodoItemCreator = () => { 21 | const [inputValue, setInputValue] = useState(''); 22 | const [priorityValue, setPriorityValue] = useState('low'); 23 | const setTodoList = useSetRecoilState(todoListState); 24 | 25 | const addItem = () => { 26 | setTodoList((oldTodoList) => [ 27 | ...oldTodoList, 28 | { 29 | id: getId(), 30 | text: inputValue, 31 | priority: priorityValue, 32 | isComplete: false, 33 | }, 34 | ]); 35 | setInputValue(''); 36 | setPriorityValue('low'); 37 | }; 38 | 39 | const onChange = ({ target: { value } }) => { 40 | setInputValue(value); 41 | }; 42 | 43 | const handleChange = (event) => { 44 | setPriorityValue(event.target.value); 45 | }; 46 | 47 | /* MUI Radio Button styles */ 48 | const GreenRadio = withStyles({ 49 | root: { 50 | color: green[400], 51 | '&$checked': { 52 | color: green[600], 53 | }, 54 | }, 55 | checked: {}, 56 | })((props) => ); 57 | 58 | const YellowRadio = withStyles({ 59 | root: { 60 | color: yellow[400], 61 | '&$checked': { 62 | color: yellow[600], 63 | }, 64 | }, 65 | checked: {}, 66 | })((props) => ); 67 | 68 | const RedRadio = withStyles({ 69 | root: { 70 | color: red[400], 71 | '&$checked': { 72 | color: red[600], 73 | }, 74 | }, 75 | checked: {}, 76 | })((props) => ); 77 | 78 | return ( 79 |
80 | 87 | 88 | 89 | 90 | 97 | } value="high" /> 98 | } value="medium" /> 99 | } value="low" /> 100 | 101 | 102 | 103 | 104 | 107 |
108 | ); 109 | }; 110 | 111 | export default TodoItemCreator; 112 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at chromogen.app@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/component.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecoilRoot, useRecoilState } from 'recoil'; 3 | import { render } from '@testing-library/react'; 4 | import { ChromogenObserver } from '../src/component/ChromogenObserver.tsx'; 5 | import { ledger } from '../src/utils/ledger.ts'; 6 | import { atom } from '../src/api/api.ts'; 7 | 8 | // import {shallow} from 'enzyme'; 9 | // import {mount} from 'enzyme'; 10 | 11 | describe('chromogenObserver', () => { 12 | global.URL = { 13 | createObjectURL: () => 'http://mockURL.com', 14 | }; 15 | 16 | beforeEach(() => { 17 | console.error = jest.fn(); 18 | // creating a mockAtom 19 | const mockAtom = atom({ key: 'mockAtom', default: true }); 20 | // create a functional mockComponent 21 | const MockComponent = () => { 22 | // declaring a React Hook using mockAtom as recoilState 23 | const [mock, setMock] = useRecoilState(mockAtom); 24 | // render a mock-button that toggles mock recoilState onclick 25 | return
170 | 181 |
182 | 183 | )} 184 | 190 | Download Test 191 | 192 | 193 | ); 194 | }; -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/output-utils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | initializeAtoms, 3 | assertState, 4 | testSelectors, 5 | testSetters, 6 | importRecoilFamily, 7 | atomFamilyHook, 8 | //writeableHook, 9 | readableHook, 10 | } from '../src/output/output-utils.ts'; 11 | 12 | // testing ternary operator in initializeAtoms helper function 13 | describe('initializeAtoms', () => { 14 | // create mock atomUpdate object, follows AtomUpdate interface 15 | const atomUpdate = { 16 | key: 'testAtom', 17 | value: 2, 18 | previous: 1, 19 | updated: true, 20 | }; 21 | 22 | it('should set correct atom value if current is true', () => { 23 | // create variable to hold evaluated result of invoking initializeAtoms on the mock array with a parameter of true 24 | const returnString = initializeAtoms([atomUpdate], true); 25 | // verify that returnString contains key property of a string and contains a value property on the atomUpdate object (since true was passed into 'initializeAtoms') 26 | expect(returnString).toEqual( 27 | expect.stringContaining(`result.current.set${atomUpdate.key}(${atomUpdate.value})`), 28 | ); 29 | }); 30 | 31 | it('should set correct atom value if current is false', () => { 32 | // create variable to hold evaluated result of invoking initializeAtoms on the mock array with a parameter of false 33 | const returnString = initializeAtoms([atomUpdate], false); 34 | // verify that returnString contains key property of a string and contains a value property on the atomUpdate object (since false was passed into 'initializeAtoms') 35 | expect(returnString).toEqual( 36 | expect.stringContaining(`result.current.set${atomUpdate.key}(${atomUpdate.previous})`), 37 | ); 38 | }); 39 | }); 40 | 41 | describe('assertState', () => { 42 | // create mock selectors array, follows SelectorUpdate interface 43 | const selectorUpdates = [ 44 | { 45 | key: 'testSelector1', 46 | value: true, 47 | }, 48 | { 49 | key: 'testSelector2', 50 | value: 100, 51 | }, 52 | ]; 53 | 54 | it('should assert on each selector value', () => { 55 | // create variable to hold evaluated result of invoking assertState on mock array 56 | const returnString = assertState(selectorUpdates); 57 | // verify that output test contains a string checking that the object's key equals a stringified version of its value on the same object 58 | expect(returnString).toEqual( 59 | expect.stringContaining( 60 | `expect(result.current.${selectorUpdates[0].key}Value).toStrictEqual(${JSON.stringify( 61 | selectorUpdates[0].value, 62 | )});`, 63 | ), 64 | ); 65 | 66 | expect(returnString).toEqual( 67 | expect.stringContaining( 68 | `expect(result.current.${selectorUpdates[1].key}Value).toStrictEqual(${JSON.stringify( 69 | selectorUpdates[1].value, 70 | )});`, 71 | ), 72 | ); 73 | }); 74 | }); 75 | 76 | describe('importRecoilFamily', () => { 77 | const familyObj = { 78 | familyName: 'string', 79 | atomName: 'test', 80 | }; 81 | it('should return a string with an object as its parameter', () => { 82 | expect(typeof importRecoilFamily(familyObj)).toBe('string'); 83 | }); 84 | }); 85 | 86 | describe('readableHook', () => { 87 | const keyArray = ['one', 'two', 'chromogen']; 88 | it('should return a string', () => { 89 | expect(typeof readableHook(keyArray)).toBe('string'); 90 | }); 91 | }); 92 | 93 | // describe('writeableHook', () => { 94 | // const keyArray = ['chromo', 'gen', 'chromogen']; 95 | // it('should return a string', () => { 96 | // expect(typeof writeableHook(keyArray)).toBe('string'); 97 | // }); 98 | // }); 99 | 100 | describe('testSelectors', () => { 101 | it('should scrub special characters from key names', () => { 102 | // create instance of invoking testSelectors on mock array that follows the Transaction interface 103 | const returnString = testSelectors([ 104 | { 105 | state: [ 106 | { 107 | key: 'atom1', 108 | value: 1, 109 | previous: 2, 110 | updated: true, 111 | }, 112 | ], 113 | updates: [ 114 | { 115 | key: 'selector1', 116 | value: 3, 117 | }, 118 | ], 119 | atomFamilyState: [ 120 | { 121 | family: 'familyName1', 122 | key: 'spec!alCh@r', 123 | value: 4, 124 | updated: true, 125 | }, 126 | ], 127 | familyUpdates: [ 128 | { 129 | key: 'familyUpdate1', 130 | value: 5, 131 | params: 'params', 132 | }, 133 | ], 134 | }, 135 | ]); 136 | // verify that if key property's value is a string with special characters they will be removed 137 | expect(returnString).toEqual(expect.not.stringContaining('spec!alCh@r')); 138 | }); 139 | }); 140 | 141 | // covers branch test percentage in testSetters 142 | describe('testSetters', () => { 143 | // create mock array with setter object 144 | const setTransactionsArrayWithSetter = [ 145 | { 146 | state: [ 147 | { 148 | key: 'atom1', 149 | value: 1, 150 | previous: 0, 151 | updated: true, 152 | }, 153 | ], 154 | setter: { 155 | key: 'selector1', 156 | value: 2, 157 | params: 'spec!alCh@r', 158 | }, 159 | }, 160 | ]; 161 | // create mock array without setter object 162 | const setTransactionsArrayWithoutSetter = [ 163 | { 164 | state: [ 165 | { 166 | key: 'atom1', 167 | value: 1, 168 | previous: 0, 169 | updated: true, 170 | }, 171 | ], 172 | }, 173 | ]; 174 | const truthyReturnString = testSetters(setTransactionsArrayWithSetter); 175 | const falsyReturnString = testSetters(setTransactionsArrayWithoutSetter); 176 | 177 | it('should scrub special characters from params', () => { 178 | // verify that if params property's value is a string with special characters, they will be removed 179 | expect(truthyReturnString).toEqual(expect.not.stringContaining('spec!alCh@r')); 180 | }); 181 | it('should return a string if an array is passed in', () => { 182 | // verify that a string is returned if provided an array with out a setter object 183 | expect(typeof falsyReturnString).toBe('string'); 184 | }); 185 | }); 186 | 187 | // TEST FOR ATOMFAMILYHOOK lines 70-81 188 | //create mock transactionArray 189 | describe('atomFamilyHook', () => { 190 | const transactionArray = [ 191 | { 192 | atomFamilyState: [ 193 | { 194 | key: 'spec!alCh@rspec!alCh@r', 195 | family: 'familyName', 196 | value: 10, 197 | updated: true, 198 | }, 199 | ], 200 | familyUpdates: [ 201 | { 202 | key: 'familyUpdate1', 203 | value: 5, 204 | params: 'params', 205 | }, 206 | ], 207 | }, 208 | ]; // truthy 209 | 210 | const transactionArray2 = []; // falsy 211 | 212 | const truthyReturnStr = atomFamilyHook(transactionArray); 213 | const falsyReturnStr = atomFamilyHook(transactionArray2); 214 | 215 | it('should scrub special characters', () => { 216 | expect(truthyReturnStr).toEqual(expect.not.stringContaining('spec!alCh@r')); 217 | }); 218 | 219 | it('should return empty string when transactionsArr length is falsy', () => { 220 | expect(falsyReturnStr).toBe(''); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /package/recoil_generator/src/component/ChromogenObserver.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Snapshot } from 'recoil'; 3 | import type { AtomFamilyState } from '../types'; 4 | 5 | import React, { useState, useEffect } from 'react'; 6 | import { useRecoilState, useRecoilTransactionObserver_UNSTABLE } from 'recoil'; 7 | import { dummyParam } from '../utils/utils'; 8 | import { recordingState } from '../utils/store'; 9 | import { ledger } from '../utils/ledger'; 10 | import { styles, generateFile } from './component-utils'; 11 | /* eslint-enable */ 12 | 13 | export const ChromogenObserver: React.FC<{ store?: Array | object }> = ({ store }) => { 14 | // Initializing as undefined over null to match React typing for AnchorHTML attributes 15 | const [file, setFile] = useState(undefined); 16 | const [storeMap, setStoreMap] = useState>(new Map()); 17 | const [recording, setRecording] = useRecoilState(recordingState); 18 | const [devtool, setDevtool] = useState(false); 19 | 20 | // DevTool message handling 21 | const receiveMessage = (message: any) => { 22 | switch (message.data.action) { 23 | case 'connectChromogen': 24 | setDevtool(true); 25 | window.postMessage({ action: 'moduleConnected' }, '*'); 26 | break; 27 | case 'downloadFile': 28 | generateFile(setFile, storeMap); 29 | break; 30 | case 'toggleRecord': 31 | setRecording(!recording); 32 | window.postMessage({ action: 'setStatus' }, '*'); 33 | break; 34 | default: 35 | // Do nothing 36 | } 37 | }; 38 | 39 | // Add/remove DevTool event listeners 40 | useEffect(() => { 41 | window.addEventListener('message', receiveMessage); 42 | 43 | return () => window.removeEventListener('message', receiveMessage); 44 | }); 45 | 46 | // Auto-click download link when a new file is generated (via button click) 47 | useEffect(() => document.getElementById('chromogen-download')!.click(), [file]); 48 | // ! to get around strict null check in tsconfig 49 | 50 | // Update storeMap with src variable names if store prop passed 51 | useEffect(() => { 52 | if (store !== undefined) { 53 | const storeArr = Array.isArray(store) ? store : [store]; 54 | const newStore: Map = new Map(); 55 | 56 | storeArr.forEach((storeModule) => { 57 | Object.entries(storeModule).forEach(([variable, imported]) => { 58 | let key; 59 | /** Relevant imports will be either an object (for vanilla atoms or selectors) 60 | * or functions (for atom or selector families). If we are examining a family function, 61 | * we will need to invoke it to create an atom/selector in order to pull the 62 | * original family key out from the generated atom or selector's individual key. 63 | * */ 64 | if (typeof imported === 'function') { 65 | // Extended atom fam key will follow format of `[key]__"chromogenDummyParam"__withFallback` 66 | // Extended selector fam key will follow format of `[key]__selectorFamily/"chromogenDummyParam"/1` 67 | const extendedKey = imported(dummyParam).key; 68 | key = extendedKey.includes('selectorFamily') 69 | ? extendedKey.substring(0, extendedKey.indexOf('selectorFamily') - 2) 70 | : extendedKey.substring(0, extendedKey.indexOf(`"${dummyParam}"`) - 2); 71 | } else { 72 | key = imported.key; 73 | } 74 | newStore.set(key, variable); 75 | }); 76 | }); 77 | setStoreMap(newStore); 78 | } 79 | }, []); 80 | 81 | useRecoilTransactionObserver_UNSTABLE( 82 | ({ previousSnapshot, snapshot }: { previousSnapshot: Snapshot; snapshot: Snapshot }): void => { 83 | // Map current snapshot to array of atom states 84 | // Can't directly check recording hook b/c TransactionObserver runs before state update 85 | if (snapshot.getLoadable(recordingState).contents) { 86 | const { transactions, setTransactions, atoms, atomFamilies } = ledger; 87 | 88 | const state = atoms.map((item) => { 89 | const { key } = item; 90 | const value = snapshot.getLoadable(item).contents; 91 | const previous = previousSnapshot.getLoadable(item).contents; 92 | const updated = value !== previous; 93 | return { key, value, previous, updated }; 94 | }); 95 | 96 | const atomFamilyState: AtomFamilyState[] = []; 97 | 98 | /* eslint-disable */ 99 | // TODO: refactor out of for-in syntax b/c for-in tracks up the prototype chain x_x 100 | for (const family in atomFamilies) { 101 | const familyMembers = atomFamilies[family]; 102 | for (const member in familyMembers) { 103 | const memberRecoilState = familyMembers[member]; 104 | let { key } = memberRecoilState; 105 | /* Key will be auto-generated by recoil in the format of 106 | * [atomFamilyName] + "__" + [params] + "__withFallback". 107 | * Removing the "__withFallback" suffix to enhance readability 108 | */ 109 | key = key.substring(0, key.length - 14); 110 | const value = snapshot.getLoadable(memberRecoilState).contents; 111 | const previous = previousSnapshot.getLoadable(memberRecoilState).contents; 112 | const updated = value !== previous; 113 | // Don't track dummy atom generated by onload useEffect hook 114 | if (!key.includes(dummyParam)) atomFamilyState.push({ family, key, value, updated }); 115 | } 116 | } 117 | /* eslint-enable */ 118 | 119 | transactions.push({ state, updates: [], atomFamilyState, familyUpdates: [] }); 120 | setTransactions.push({ state, setter: null }); 121 | } 122 | }, 123 | ); 124 | 125 | const [pauseColor, setPauseColor] = useState('#90d1f0'); 126 | const pauseBorderStyle = { 127 | borderColor: `${pauseColor}`, 128 | }; 129 | 130 | const [playColor, setPlayColor] = useState('transparent transparent transparent #90d1f0') 131 | const playBorderStyle = { 132 | borderColor: `${playColor}`, 133 | }; 134 | 135 | return ( 136 | <> 137 | { 138 | // Render button div only if DevTool not connected 139 | !devtool && ( 140 |
141 |
142 | 157 | 167 |
168 |
169 | ) 170 | } 171 | 177 | Download Test 178 | 179 | 180 | ); 181 | }; 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Chromogen logo 10 | 11 | 12 |

A UI-driven test-generation package for Recoil selectors.

13 | 14 |
15 | 16 | 17 | [![npm version](https://img.shields.io/npm/v/chromogen)](https://www.npmjs.com/package/chromogen) 18 | [![Build Status](https://travis-ci.com/open-source-labs/Chromogen.svg?branch=master)](https://travis-ci.org/oslabs-beta/Chromogen) 19 | [![Coverage Status](https://coveralls.io/repos/github/open-source-labs/Chromogen/badge.svg?branch=master)](https://coveralls.io/github/oslabs-beta/Chromogen?branch=master) 20 | [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/oslabs-beta/Chromogen/blob/master/LICENSE) 21 | 22 | [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=CHROMOGEN%20-%20A%20UI-driven%20Jest%20test%20generator%20for%20Recoil%20apps%0A&url=https://www.npmjs.com/package/Chromogen&hashtags=React,Recoil,Jest,testing) 23 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) 24 | [![npm downloads](https://img.shields.io/npm/dm/chromogen)](https://www.npmjs.com/package/chromogen) 25 | [![Github stars](https://img.shields.io/github/stars/oslabs-beta/Chromogen?style=social)](https://github.com/oslabs-beta/Chromogen) 26 | 27 |
28 | 29 | ## Table of Contents 30 | 31 | - [Overview](#overview) 32 | - [Installation](#installation) 33 | - [Usage](#usage) 34 | - [Contributing](#contributing) 35 | - [Core Team](#core-team) 36 | - [License](#license) 37 | 38 | ## Overview 39 | 40 | You're an independent developer or part of a lean team. You want reliable unit tests for your new React-Recoil app, but you need to move fast and time is major constraint. More importantly, you want your tests to reflect how your users interact with the application, rather than testing implementation details. 41 | 42 | [Enter Chromogen](https://www.npmjs.com/package/chromogen). Chromogen is a Jest unit-test generation tool for Recoil selectors. It captures state changes during user interaction and auto-generates corresponding test suites. Simply launch your application (after following the installation instructions below), interact as a user normally would, and with one click you'll download a ready-to-run Jest test file. 43 | 44 | ### Don't have a Recoil app handy? 45 | Chromogen's [official demo app](demo-todo/README.md) provides a ready-to-run Recoil frontend with a number of different selector implementations to test against. It's available in the `demo-todo` folder of this repository and comes with Chromogen pre-installed; just run `npm install && npm start` to launch. 46 | 47 | ### Chromogen is currently in active Beta 48 | Chromogen supports three main types of test: 49 | 1. **Initial selector values** on page load 50 | 2. **Selector return values** for a given state, using snapshots captured after each state transaction. 51 | 3. **Selector _set_ logic** asserting on resulting atom values for a given `newValue` argument and starting state. 52 | 53 | These test suites will be captured for _synchronous_ selectors and selectorFamilies only. However, the presence of asyncronous selectors in your app should not cause any issues with the generated tests. Chromogen can identify such selectors at run-time and exclude them from capture. 54 | 55 | At this time, we have no plans to introduce testing for async selectors; the mocking requirements are too opaque and fragile to accurately capture at runtime. However, we are always open to suggestions to meet the needs of our userbase. Want to see this or any other feature added to the package? [Let us know!](#contributing) 56 | 57 | By default, Chromogen uses atom and selector keys to populate the import & hook statements in the test file. If your source code does _not_ use matching variable and key names, you will need to pass the imported atoms and selectors to the ChromogenObserver component as a `store` prop. The installation instructions below contain further details. 58 | 59 | _(09/15/20)_ **WARNING:** _Chromogen_ v1.3.x is only compatible with Recoil v0.0.10 currently. We are working on an update to enable compatibility with Recoil's new v0.0.11 release. 60 | 61 | ## Installation 62 | 63 | Before running Chromogen, you'll need to make two changes to your application: 64 | 1. Import the `` component as a child of `` 65 | 1. Import the `atom` and `selector` functions from Chromogen instead of Recoil 66 | 67 | These changes do have a small performance cost, so they should be reverted before deploying to production. 68 | 69 | ### Download the Chromogen package from npm. 70 | 71 | ``` 72 | npm install chromogen 73 | ``` 74 | 75 | ### Import the ChromogenObserver component 76 | ChromogenObserver should be included as a direct child of RecoilRoot. It does not need to wrap any other components, and it takes no mandatory props. It utilizes Recoil's TransactionObserver hook to record snapshots on state change. 77 | 78 | ```jsx 79 | import React from 'react'; 80 | import { RecoilRoot } from 'recoil'; 81 | import { ChromogenObserver } from 'chromogen'; 82 | import MyComponent from './components/MyComponent.jsx'; 83 | 84 | const App = (props) => ( 85 | 86 | 87 | 88 | 89 | ) 90 | 91 | export default App; 92 | ``` 93 | 94 | If you are using pseudo-random key names, such as with _UUID_, you'll need to pass all of your store exports to the ChromogenObserver component as a `store` prop. This will allow Chromogen to use source code variable names in the output file, instead of relying on keys. When all atoms and selectors are exported from a single file, you can pass the imported module directly: 95 | ```jsx 96 | import * as store from './store'; 97 | // ... 98 | 99 | ``` 100 | 101 | If your store utilizes seprate files for various pieces of state, you can pass all of the imports in an array: 102 | ```jsx 103 | import * as atoms from './store/atoms'; 104 | import * as selectors from './store/selectors'; 105 | import * as misc from './store/arbitraryRecoilState'; 106 | // ... 107 | 108 | ``` 109 | 110 | ### Import atom & selector functions from Chromogen 111 | Wherever you import `atom` and/or `selector` functions from Recoil (typically in your `store` file), import them from Chromogen instead. The arguments passed in do **not** need to change in any away, and the return value will still be a normal RecoilAtom or RecoilSelector. Chromogen wraps the native Recoil functions to track which pieces of state have been created, as well as when various selectors are called and what values they return. 112 | 113 | ```js 114 | import { atom, selector } from 'chromogen'; 115 | 116 | export const fooState = atom({ 117 | key: 'fooState', 118 | default: {}, 119 | }); 120 | 121 | export const barState = selector({ 122 | key: 'barState', 123 | get: ({ get }) => { 124 | const derivedState = get(fooState); 125 | return derivedState.baz || 'value does not exist'; 126 | } 127 | }) 128 | ``` 129 | 130 | ## Usage 131 | After following the installation steps above, launch your application as normal. You should see two buttons in the bottom left corner. 132 | 133 |
134 | 135 | ![Buttons](./assets/README-root/buttons.png) 136 | 137 |
138 | 139 | The green button, on the left, is the **download** button. Clicking it will download a new test file that includes _all_ tests generated since the app was last launched or refreshed. 140 | 141 | The red button, on the right, is the **recording toggle**. Clicking it will pause recording, so that no tests are generated during subsequent state changes. Red indicates "recording in progress" and yellow means the recording is paused. Pausing is useful for setting up a complex initial state with repetitive actions, where you don't want to test every step of the process. 142 | 143 | For example, if we want to test our to-do app's filter and sort buttons, we may want to have 10 or so different items with various priority levels and completion states. However, we don't necessarily want 10 separate tests just for adding items. We can instead add one or two items to generate tests for that functionality, then pause recording while we add the other 8 items. Once everything is added, we can resume recording to generate filter & sort tests with all 10 items present. 144 | 145 | Once you've recorded all the interactions you want to test, click the green button to download the test file. You can now drag-and-drop the downloaded file into your app's test directory. 146 | 147 |
148 | 149 | 150 | ![Download](./assets/README-root/download.png)    ![File](./assets/README-root/test-directory.png) 151 | 152 |
153 | 154 | Before running the test file, you'll need to specify the import path for your store by replacing ``. The default output assumes that all atoms and selectors are imported from a single path; if that's not possible, you'll need to separately import each set of atoms and/or selectors from their appropriate path. 155 | 156 | | **BEFORE** | **AFTER** | 157 | |:----------:|:---------:| 158 | |![Default Filepath](./assets/README-root/filepath-before.png)|![Updated Filepath](./assets/README-root/filepath-after.png)| 159 | 160 | You're now ready to run your tests! Upon running your normal Jest test command, you should see three suites for `chromogen.test.js`: 161 | 162 |
163 | 164 | ![Test Output](./assets/README-root/test-output.png) 165 | 166 |
167 | 168 | **Initial Render** tests whether each selector returns the correct value at launch. There is one test per selector. 169 | 170 | **Selectors** tests the return value of various selectors for a given state. Each test represents the app state after a transaction has occured, generally triggered by some user interaction. For each selector that ran after that transaction, the test asserts on the selector's return value for the given state. 171 | 172 | **Setters** tests the state that results from setting a writeable selector with a given value and starting state. There is one test per set call, asserting on each atom's value in the resulting state. 173 | 174 | ### Chrome DevTool (Optional) 175 | If the injected buttons interfere with the functioning or layout of your application, you can also control Chromogen through an optional DevTool panel. As soon as Chromogen detects that the panel has been opened and loaded, the injected buttons will disappear from the application view. The recording and download buttons on the panel work exactly the same as outlined above. 176 | 177 |
178 | 179 | ![DevTool Panel](./assets/README-root/devtool-panel.png) 180 | 181 |
182 | 183 | _Please Note:_ Chromogen's DevTool is currently under review with the Chrome Web Store. In the interim, the DevTool can be added as an unpacked extension by running `npm install && npm run build` in the `dev-tool` subdirectory and loading the resulting `build` folder. 184 | 185 | ## Contributing 186 | **We expect all contributors to abide by the standards of behavior outlined in the [Code of Conduct](CODE_OF_CONDUCT.md).** 187 | 188 | We welcome community contributions, including new developers who've never [made an open source Pull Request before](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). If you'd like to start a new PR, we recommend [creating an issue](https://docs.github.com/en/github/managing-your-work-on-github/creating-an-issue) for discussion first. This lets us open a conversation, ensuring work is not duplicated unnecessarily and that the proposed PR is a fix or feature we're actively looking to add. 189 | 190 | ### Bugs 191 | 192 | Please [file an issue](https://docs.github.com/en/github/managing-your-work-on-github/creating-an-issue) for bugs, missing documentation, or unexpected behavior. 193 | 194 | ### Feature Requests 195 | 196 | Please file an issue to suggest new features. Vote on feature requests by adding 197 | a 👍. This helps us prioritize what to work on. 198 | 199 | ### Questions 200 | 201 | For questions related to using the package, you may either file an issue or _gmail_ us: `chromogen.app`. 202 | 203 | ## Core Team 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 |

Michelle Holland

Andy Wang

Connor Rose Delisle

Jim Chen
215 | 216 | ## LICENSE 217 | Logo remixed from [ReactJS](https://github.com/reactjs/reactjs.org) under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) and [Smashicons](https://www.flaticon.com/authors/smashicons) via [www.flaticon.com](https://www.flaticon.com/) 218 | 219 | README format adapted from [react-testing-library](https://github.com/testing-library/react-testing-library/blob/master/README.md) under [MIT license](https://github.com/testing-library/react-testing-library/blob/master/LICENSE). 220 | 221 | All Chromogen source code is [MIT](./LICENSE) licensed. 222 | 223 | Lastly, shoutout to [this repo](https://github.com/conorhastings/redux-test-recorder) for the original inspiration. 224 | -------------------------------------------------------------------------------- /package/recoil_generator/src/output/output-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { 3 | SelectorUpdate, 4 | Transaction, 5 | AtomUpdate, 6 | SetTransaction, 7 | AtomFamilies, 8 | SelectorFamilies, 9 | SelectorFamilyUpdate, 10 | SelectorFamilyMembers, 11 | } from '../types'; 12 | import { SerializableParam } from 'recoil'; 13 | /* eslint-enable */ 14 | 15 | /* ----- HELPER FUNCTIONS ----- */ 16 | 17 | export function initializeAtoms(state: AtomUpdate[], current: boolean): string { 18 | return state.reduce( 19 | (initializers, { key, value, previous }) => 20 | `${initializers}\t\t\tresult.current.set${key}(${JSON.stringify( 21 | current ? value : previous, 22 | )});\n\n`, 23 | '', 24 | ); 25 | } 26 | //chromogen only captures selectors that fire; this pulls value from latest firing 27 | export function assertState(updates: SelectorUpdate[]): string { 28 | return updates.reduce( 29 | (assertions, { key, value }) => 30 | `${assertions}\t\texpect(result.current.${key}Value).toStrictEqual(${JSON.stringify( 31 | value, 32 | )});\n\n`, 33 | '', 34 | ); 35 | } 36 | 37 | /* ----- SETUP FUNCTIONS ----- */ 38 | 39 | export function importRecoilState(keyArray: string[]): string { 40 | return keyArray.reduce((fullStr, key) => `${fullStr}\t${key},\n`, ''); 41 | } 42 | 43 | export function importRecoilFamily( 44 | familyObj: AtomFamilies | SelectorFamilies, 45 | ): string { 46 | return Object.keys(familyObj).reduce( 47 | (importStr, familyName) => `${importStr}\t${familyName},\n`, 48 | '', 49 | ); 50 | } 51 | 52 | export function writeableHook(keyArray: string[]): string { 53 | return keyArray.reduce( 54 | (fullStr, key) => `${fullStr}\tconst [${key}Value, set${key}] = useRecoilState(${key});\n`, 55 | '', 56 | ); 57 | } 58 | 59 | export function readableHook(keyArray: string[]): string { 60 | return keyArray.reduce( 61 | (fullStr, key) => `${fullStr}\tconst ${key}Value = useRecoilValue(${key});\n`, 62 | '', 63 | ); 64 | } 65 | 66 | export function atomFamilyHook(transactionArray: Transaction[]): string { 67 | const len = transactionArray.length; 68 | return len 69 | ? transactionArray[len - 1].atomFamilyState.reduce((str, atomState) => { 70 | const { family, key } = atomState; 71 | /* Removing all special characters from string: if the params are passed 72 | * in as a string, then we need to remove any special characters so they don't 73 | * error out variable name generation, also recoil will add escaped quotes 74 | * to the key name if it's a string, so will need to remove those by default 75 | */ 76 | const params = key.substring(family.length + 2); 77 | const scrubbedParams = params.replace(/[^\w\s]/gi, ''); 78 | 79 | const parsedParams = JSON.parse(params); 80 | 81 | return `${str}\tconst [${`${family}__${scrubbedParams}__Value`}, ${`set${family}__${scrubbedParams}`}] = useRecoilState(${family}(${ 82 | typeof parsedParams === 'string' ? `${params}` : `${parsedParams}` 83 | }));\n`; 84 | }, '') 85 | : ''; 86 | } 87 | 88 | export function selectorFamilyHook( 89 | selectorFamilyTracker: SelectorFamilies, 90 | isSettable: boolean, 91 | ): string { 92 | return Object.entries(selectorFamilyTracker) 93 | .filter((familyArr) => familyArr[1].isSettable === isSettable) 94 | .reduce((str: string, familyArr: [string, { prevParams: Set }]): string => { 95 | const [familyName, { prevParams }] = familyArr; 96 | // converting prevParams from set to array 97 | return `${str}${[...prevParams].reduce((innerStr: string, param: any) => { 98 | let scrubbedParams; 99 | if (typeof param === 'string') { 100 | scrubbedParams = param.replace(/[^\w\s]/gi, ''); 101 | } 102 | 103 | return isSettable 104 | ? `${innerStr}\tconst [${`${familyName}__${ 105 | scrubbedParams !== undefined ? scrubbedParams : param 106 | }__Value`}, ${`set${familyName}__${ 107 | scrubbedParams !== undefined ? scrubbedParams : param 108 | }`}] = useRecoilState(${familyName}(${ 109 | typeof param === 'string' ? `"${param}"` : `${JSON.parse(param)}` 110 | }));\n` 111 | : `${innerStr}\tconst ${`${familyName}__${ 112 | scrubbedParams !== undefined ? scrubbedParams : param 113 | }__Value`} = useRecoilValue(${familyName}(${ 114 | typeof param === 'string' ? `"${param}"` : `${JSON.parse(param)}` 115 | }));\n`; 116 | }, '')}`; 117 | }, ''); 118 | } 119 | 120 | export function returnWriteable(keyArray: string[]): string { 121 | return keyArray.reduce((fullStr, key) => `${fullStr}\t\t${key}Value,\n\t\tset${key},\n`, ''); 122 | } 123 | 124 | export function returnReadable(keyArray: string[]): string { 125 | return keyArray.reduce((fullStr, key) => `${fullStr}\t\t${key}Value,\n`, ''); 126 | } 127 | 128 | export function returnAtomFamily(transactionArray: Transaction[]): string { 129 | const len = transactionArray.length; 130 | return len 131 | ? transactionArray[len - 1].atomFamilyState.reduce((value, atomState) => { 132 | const { family, key } = atomState; 133 | // key will be "[familyname]__[params]" 134 | const params = key.substring(family.length + 2); 135 | const scrubbedParams = params.replace(/[^\w\s]/gi, ''); 136 | return `${value}\t\t${`${family}__${scrubbedParams}__Value`}, 137 | \t\t${`set${family}__${scrubbedParams}`},\n`; 138 | }, '') 139 | : ''; 140 | } 141 | 142 | export function returnSelectorFamily( 143 | selectorFamilyTracker: SelectorFamilies, 144 | isSettable: boolean, 145 | ) { 146 | return Object.entries(selectorFamilyTracker) 147 | .filter((familyArr) => familyArr[1].isSettable === isSettable) 148 | .reduce( 149 | (str: string, familyArr: [string, SelectorFamilyMembers]): string => { 150 | const [familyName, { prevParams }] = familyArr; 151 | if (isSettable) { 152 | return `${str}${[...prevParams].reduce((innerStr: string, param: any) => { 153 | let scrubbedParams; 154 | if (typeof param === 'string') { 155 | scrubbedParams = param.replace(/[^\w\s]/gi, ''); 156 | } 157 | 158 | return `${innerStr}\t\t${`${familyName}__${ 159 | scrubbedParams !== undefined ? scrubbedParams : param 160 | }__Value`}, 161 | ${`set${familyName}__${scrubbedParams !== undefined ? scrubbedParams : param}`},\n`; 162 | }, '')}`; 163 | } 164 | return `${str}${[...prevParams].reduce((innerStr: string, param: any) => { 165 | let scrubbedParams; 166 | if (typeof param === 'string') { 167 | scrubbedParams = param.replace(/[^\w\s]/gi, ''); 168 | } 169 | return `${innerStr}\t\t${`${familyName}__${ 170 | scrubbedParams !== undefined ? scrubbedParams : param 171 | }__Value`},\n`; 172 | }, '')}`; 173 | }, 174 | '', 175 | ); 176 | } 177 | 178 | /* ----- INITIAL RENDER ----- */ 179 | 180 | export function initializeSelectors(initialRender: SelectorUpdate[]): string { 181 | return initialRender.reduce( 182 | (fullStr, { key, value }) => `${fullStr}\tit('${key} should initialize correctly', () => { 183 | \t\texpect(result.current.${key}Value).toStrictEqual(${JSON.stringify(value)}); 184 | \t});\n\n`, 185 | '', 186 | ); 187 | } 188 | 189 | export function initializeSelectorFamilies(initialRenderFamilies: SelectorFamilyUpdate[]) { 190 | return initialRenderFamilies.reduce((initialTests, { key, params, value }) => { 191 | let scrubbedParams; 192 | if (typeof params === 'string') { 193 | scrubbedParams = params.replace(/[^\w\s]/gi, ''); 194 | } 195 | 196 | return `${initialTests}\tit('${key}__${ 197 | scrubbedParams !== undefined ? scrubbedParams : JSON.stringify(params) 198 | } should initialize correctly', () => { 199 | \t\texpect(result.current.${key}__${ 200 | scrubbedParams !== undefined ? scrubbedParams : JSON.stringify(params) 201 | }__Value).toStrictEqual(${JSON.stringify(value)}); 202 | \t});\n`; 203 | }, ''); 204 | } 205 | 206 | /* ----- SELECTORS TEST ----- */ 207 | //checking get methods 208 | export function testSelectors(transactionArray: Transaction[]): string { 209 | return transactionArray.reduce( 210 | (selectorTests, { state, updates, atomFamilyState, familyUpdates }) => { 211 | //checking to make sure chromogen doesn't look at atoms that haven't changed state 212 | const allUpdatedAtoms = [ 213 | ...state.filter(({ updated }) => updated), 214 | ...atomFamilyState.filter(({ updated }) => updated), 215 | ]; 216 | const allUpdatedSelectors: any[] = [...updates, ...familyUpdates]; 217 | const atomLen = allUpdatedAtoms.length; 218 | const selectorLen = allUpdatedSelectors.length; 219 | 220 | return atomLen !== 0 && selectorLen !== 0 221 | ? `${selectorTests}\tit('${ 222 | selectorLen > 1 223 | ? allUpdatedSelectors.reduce((list, selectorState, i) => { 224 | const { key } = selectorState; 225 | const isLastElement = i === selectorLen - 1; 226 | // if params exist, then we are looking at a selectorFamily 227 | if ('params' in selectorState) { 228 | let scrubbedParams; 229 | if (typeof selectorState.params === 'string') { 230 | scrubbedParams = selectorState.params.replace(/[^\w\s]/gi, ''); 231 | } 232 | 233 | return `${list}${isLastElement ? 'and ' : ''}${key}__${ 234 | scrubbedParams !== undefined ? scrubbedParams : selectorState.params 235 | }${isLastElement ? '' : ', '}`; 236 | } 237 | return `${list}${isLastElement ? 'and ' : ''}${key}${isLastElement ? '' : ', '}`; 238 | }, '') 239 | : `${ 240 | allUpdatedSelectors[0].params !== undefined 241 | ? `${allUpdatedSelectors[0].key}__${ 242 | typeof allUpdatedSelectors[0].params === 'string' 243 | ? allUpdatedSelectors[0].params.replace(/[^\w\s]/gi, '') 244 | : allUpdatedSelectors[0].params 245 | }` 246 | : allUpdatedSelectors[0].key 247 | }` 248 | } should properly derive state when ${ 249 | atomLen > 1 250 | ? allUpdatedAtoms.reduce((list, { key }, i) => { 251 | const isLastElement = i === atomLen - 1; 252 | const scrubbedKey = key.replace(/[^\w\s]/gi, ''); 253 | return `${list}${isLastElement ? 'and ' : ''}${scrubbedKey}${ 254 | isLastElement ? ' update' : ', ' 255 | }`; 256 | }, '') 257 | : `${allUpdatedAtoms[0].key.replace(/[^\w\s]/gi, '')} updates` 258 | }', () => { 259 | \t\tconst { result } = renderRecoilHook(useStoreHook); 260 | 261 | \t\tact(() => { 262 | ${state.reduce( 263 | (initializers, { key, value }) => 264 | `${initializers}\t\t\tresult.current.set${key}(${JSON.stringify(value)});\n\n`, 265 | '', 266 | )} 267 | ${atomFamilyState.reduce((initializers, { key, value }) => { 268 | const scrubbedKey = key.replace(/[^\w\s]/gi, ''); 269 | return `${initializers}\t\t\tresult.current.set${scrubbedKey}(${JSON.stringify(value)});\n\n`; 270 | }, '')} 271 | \t\t}); 272 | ${ 273 | selectorLen !== 0 274 | ? allUpdatedSelectors.reduce((assertions, selectorState) => { 275 | const { key, value } = selectorState; 276 | 277 | let scrubbedParams; 278 | if (typeof selectorState.params === 'string') { 279 | scrubbedParams = selectorState.params.replace(/[^\w\s]/gi, ''); 280 | } 281 | 282 | if (selectorState.params !== undefined) 283 | return `${assertions}\t\texpect(result.current.${key}__${ 284 | scrubbedParams !== undefined ? scrubbedParams : selectorState.params 285 | }__Value).toStrictEqual(${JSON.stringify(value)});\n\n`; 286 | return `${assertions}\t\texpect(result.current.${key}Value).toStrictEqual(${JSON.stringify( 287 | value, 288 | )});\n\n`; 289 | }, '') 290 | : '' 291 | }\t});\n\n` 292 | : selectorTests; 293 | }, 294 | '', 295 | ); 296 | } 297 | 298 | /* ----- SETTERS TEST ----- */ 299 | 300 | export function testSetters(setTransactionArray: SetTransaction[]): string { 301 | return setTransactionArray.reduce((setterTests, { state, setter }) => { 302 | const updatedAtoms = state.filter(({ updated }) => updated); 303 | 304 | if (setter) { 305 | const { params } = setter; 306 | 307 | let scrubbedParams; 308 | if (typeof params === 'string') { 309 | scrubbedParams = params.replace(/[^\w\s]/gi, ''); 310 | } 311 | 312 | return params !== undefined 313 | ? `${setterTests}\tit('${setter.key}__${ 314 | scrubbedParams !== undefined ? scrubbedParams : JSON.stringify(params) 315 | } should properly set state', () => { 316 | \t\tconst { result } = renderRecoilHook(useStoreHook); 317 | 318 | \t\tact(() => { 319 | ${initializeAtoms(state, false)}\t\t}); 320 | 321 | \t\tact(() => { 322 | \t\t\tresult.current.set${setter.key}__${ 323 | scrubbedParams !== undefined ? scrubbedParams : JSON.stringify(params) 324 | }(${JSON.stringify(setter.newValue)}); 325 | \t\t}); 326 | 327 | ${assertState(updatedAtoms)}\t});\n\n` 328 | : `${setterTests}\tit('${setter.key} should properly set state', () => { 329 | \t\tconst { result } = renderRecoilHook(useStoreHook); 330 | 331 | \t\tact(() => { 332 | ${initializeAtoms(state, false)}\t\t}); 333 | 334 | \t\tact(() => { 335 | \t\t\tresult.current.set${setter.key}(${JSON.stringify(setter.newValue)}); 336 | \t\t}); 337 | 338 | ${assertState(updatedAtoms)}\t});\n\n`; 339 | } 340 | return setterTests; 341 | }, ''); 342 | } 343 | --------------------------------------------------------------------------------