├── package ├── zustand_generator │ ├── src │ │ ├── utils │ │ │ ├── utils.ts │ │ │ ├── ledger.ts │ │ │ └── store.ts │ │ ├── types.ts │ │ ├── component │ │ │ ├── EditorTab.tsx │ │ │ ├── Buttons │ │ │ │ ├── RecordingButton.tsx │ │ │ │ ├── RecordingVariations │ │ │ │ │ ├── Start.tsx │ │ │ │ │ └── Record.tsx │ │ │ │ └── SecondaryButton.tsx │ │ │ ├── Numbers.tsx │ │ │ ├── ChromogenZustandObserver.tsx │ │ │ ├── Resizing │ │ │ │ └── Resizer.tsx │ │ │ ├── Editor.tsx │ │ │ ├── component-utils.ts │ │ │ ├── Icons.tsx │ │ │ ├── panel.tsx │ │ │ └── Header.tsx │ │ ├── output │ │ │ ├── output.ts │ │ │ └── output-utils.ts │ │ ├── GlobalStyle.ts │ │ └── api │ │ │ └── api.ts │ └── __tests__ │ │ ├── api.test.js │ │ └── output-utils.test.js ├── 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 │ │ ├── types.ts │ │ └── component │ │ │ └── component-utils.ts │ └── __tests__ │ │ ├── core-utils.test.jx │ │ ├── component-utils.test.js │ │ ├── output.test.js │ │ ├── utils.test.js │ │ ├── api.test.js │ │ ├── component.test.js │ │ └── output-utils.test.js ├── index.ts ├── LICENSE ├── tsconfig.json ├── package.json └── README.md ├── assets ├── logo │ └── Chromogen.png └── README-root │ ├── test-output.png │ ├── filepath-after.png │ ├── filepath-before.png │ ├── ultratrimmedDemo.gif │ ├── zustand-test-filepath-1.png │ ├── zustand-test-filepath-2.png │ └── zustand-test-snapshot-2.png ├── demo-todo ├── src │ ├── favicon.ico │ ├── index.js │ ├── components │ │ ├── App.jsx │ │ ├── Quotes.jsx │ │ ├── TodoQuickCheck.jsx │ │ ├── ReadOnlyTodoItem.jsx │ │ ├── TodoList.jsx │ │ ├── SearchBar.jsx │ │ ├── TodoItem.jsx │ │ ├── TodoItemCreator.jsx │ │ └── TodoListFilters.jsx │ ├── index.html │ ├── store │ │ ├── atoms.js │ │ └── store.js │ └── styles │ │ └── styles.css ├── .babelrc ├── README.md ├── webpack.config.js ├── LICENSE ├── package.json └── __tests__ │ └── initialTestTest.js ├── demo-zustand-todo ├── src │ ├── favicon.ico │ ├── index.js │ ├── components │ │ ├── App.jsx │ │ ├── TodoQuickCheck.jsx │ │ ├── ReadOnlyTodoItem.jsx │ │ ├── Quotes.jsx │ │ ├── TodoItem.jsx │ │ ├── SearchBar.jsx │ │ ├── TodoList.jsx │ │ ├── TodoItemCreator.jsx │ │ └── TodoListFilters.jsx │ ├── index.html │ ├── store │ │ └── store.js │ └── styles │ │ └── styles.css ├── chromogen-4.0.4.tgz ├── .babelrc ├── webpack.config.js ├── LICENSE ├── package.json └── __tests__ │ └── sampleTest.js ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── .travis.yml ├── .gitignore ├── jenkins ├── scripts │ ├── kill.sh │ ├── test.sh │ └── deliver.sh └── Jenkinsfile ├── .prettierrc.json ├── Dockerfile ├── LICENSE ├── .eslintrc.json ├── CODE_OF_CONDUCT.md └── package.json /package/zustand_generator/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const dummyParam = 'chromogenDummyParam'; 2 | -------------------------------------------------------------------------------- /assets/logo/Chromogen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/assets/logo/Chromogen.png -------------------------------------------------------------------------------- /demo-todo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/demo-todo/src/favicon.ico -------------------------------------------------------------------------------- /demo-zustand-todo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/demo-zustand-todo/src/favicon.ico -------------------------------------------------------------------------------- /assets/README-root/test-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/assets/README-root/test-output.png -------------------------------------------------------------------------------- /assets/README-root/filepath-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/assets/README-root/filepath-after.png -------------------------------------------------------------------------------- /assets/README-root/filepath-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/assets/README-root/filepath-before.png -------------------------------------------------------------------------------- /demo-zustand-todo/chromogen-4.0.4.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/demo-zustand-todo/chromogen-4.0.4.tgz -------------------------------------------------------------------------------- /assets/README-root/ultratrimmedDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/assets/README-root/ultratrimmedDemo.gif -------------------------------------------------------------------------------- /assets/README-root/zustand-test-filepath-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/assets/README-root/zustand-test-filepath-1.png -------------------------------------------------------------------------------- /assets/README-root/zustand-test-filepath-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/assets/README-root/zustand-test-filepath-2.png -------------------------------------------------------------------------------- /assets/README-root/zustand-test-snapshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/HEAD/assets/README-root/zustand-test-snapshot-2.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 | } -------------------------------------------------------------------------------- /demo-zustand-todo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env"], 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | //"react-hot-loader/babel" 8 | ] 9 | } -------------------------------------------------------------------------------- /package/zustand_generator/src/utils/ledger.ts: -------------------------------------------------------------------------------- 1 | import type { Ledger } from '../types'; 2 | 3 | export const ledger: Ledger = { 4 | initialRender: {}, 5 | transactions: [], 6 | }; 7 | -------------------------------------------------------------------------------- /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 | - 14 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 | -------------------------------------------------------------------------------- /.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 | # Demo App 15 | chromogen.test.js 16 | package-lock.json 17 | TODO.md -------------------------------------------------------------------------------- /jenkins/scripts/kill.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo 'The following command terminates the "npm start" process using its PID' 4 | echo '(written to ".pidfile"), all of which were conducted when "deliver.sh"' 5 | echo 'was executed.' 6 | set -x 7 | kill $(cat .pidfile) 8 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | import React from 'react'; 3 | import App from './components/App'; 4 | import { createRoot } from 'react-dom/client'; 5 | 6 | const root = createRoot(document.getElementById('app')); 7 | 8 | root.render(); 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | import { createRoot } from'react-dom/client'; 6 | 7 | const root = createRoot(document.getElementById('app')); 8 | 9 | root.render( 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChromogenZustandObserver } from 'chromogen'; 3 | import TodoList from './TodoList'; 4 | import '../styles/styles.css'; 5 | 6 | const App = () => ( 7 | <> 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/core-utils.test.jx: -------------------------------------------------------------------------------- 1 | import { 2 | debouncedAddToTransactions, 3 | wrapGetter, 4 | wrapSetter, 5 | } from '../src/api/core-utils'; 6 | 7 | import { debounce } from '../src/utils/utils'; 8 | 9 | xdescribe('debouncedAddToTransaction', () => { 10 | 11 | }); 12 | 13 | xdescribe('wrapGetter', () => { 14 | 15 | }); 16 | 17 | xdescribe('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/zustand_generator/src/utils/store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | 3 | interface RecordingState { 4 | recording: boolean; 5 | toggleRecording: () => void; 6 | } 7 | 8 | /* 9 | Allows for recording to always be on during page load 10 | and the ability to pause recording 11 | */ 12 | export const useStore = create((set) => ({ 13 | recording: true, 14 | toggleRecording: () => { 15 | set((state) => ({ recording: !state.recording }), false); 16 | }, 17 | })); 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package/zustand_generator/src/types.ts: -------------------------------------------------------------------------------- 1 | // ----- EXPORTING TYPES TO BE USED IN SRC/.TSX FILES ----- 2 | 3 | type NotAFunction = { [k: string]: unknown } & ({ bind?: never } | { call?: never }); 4 | 5 | export type InitialRender = { 6 | [stateParam: string]: NotAFunction; 7 | }; 8 | 9 | export interface Transaction { 10 | action: string; 11 | arguments?: T; 12 | changedValues: { 13 | [nameOfChangedValue: string]: NotAFunction; 14 | }; 15 | } 16 | 17 | export interface Ledger { 18 | initialRender: InitialRender; 19 | transactions: Transaction[]; 20 | } 21 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/EditorTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SecondaryButton from './Buttons/SecondaryButton'; 3 | 4 | type Props = { 5 | setIsHidden: Function; 6 | isHidden: boolean; 7 | }; 8 | 9 | const EditorTab = (props: Props): JSX.Element => { 10 | const { isHidden, setIsHidden } = props; 11 | return ( 12 |
13 | setIsHidden(!isHidden)} /> 14 |
15 | ); 16 | }; 17 | 18 | export default EditorTab; 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jenkins/jenkins:2.375.3 2 | USER root 3 | RUN apt-get update && apt-get install -y lsb-release 4 | RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \ 5 | https://download.docker.com/linux/debian/gpg 6 | RUN echo "deb [arch=$(dpkg --print-architecture) \ 7 | signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \ 8 | https://download.docker.com/linux/debian \ 9 | $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list 10 | RUN apt-get update && apt-get install -y docker-ce-cli 11 | USER jenkins 12 | RUN jenkins-plugin-cli --plugins "blueocean docker-workflow" -------------------------------------------------------------------------------- /demo-zustand-todo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Chromogen Zustand Demo To-Do 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 10 | return ( 11 | <> 12 |
13 |

{quoteText}

14 | 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Quotes; 23 | -------------------------------------------------------------------------------- /package/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { atom, selector, atomFamily, selectorFamily } from './recoil_generator/src/api/api'; 3 | import { ChromogenZustandObserver } from './zustand_generator/src/component/ChromogenZustandObserver'; 4 | import { ChromogenObserver } from './recoil_generator/src/component/ChromogenObserver'; 5 | import { chromogenZustandMiddleware } from './zustand_generator/src/api/api'; 6 | import Editor from './zustand_generator/src/component/Editor'; 7 | 8 | // CHROMGOEN FAMILY APIs ARE CURRENTLY UNSTABLE 9 | export { 10 | atom, 11 | selector, 12 | atomFamily, 13 | selectorFamily, 14 | ChromogenObserver, 15 | chromogenZustandMiddleware, 16 | ChromogenZustandObserver, 17 | Editor, 18 | }; 19 | -------------------------------------------------------------------------------- /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 | xdescribe('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 | -------------------------------------------------------------------------------- /jenkins/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | image 'node:lts-buster-slim' 5 | args '-p 3003:3003' 6 | } 7 | } 8 | environment { 9 | CI = 'true' 10 | } 11 | stages { 12 | stage('Build') { 13 | steps { 14 | sh 'npm --prefix ./package install' 15 | } 16 | } 17 | stage('Test') { 18 | steps { 19 | sh './jenkins/scripts/test.sh' 20 | } 21 | } 22 | stage('Deliver') { 23 | steps { 24 | sh './jenkins/scripts/deliver.sh' 25 | input message: 'Finished using the web site? (Click "Proceed" to continue)' 26 | sh './jenkins/scripts/kill.sh' 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Buttons/RecordingButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import RecordButton from './RecordingVariations/Record'; 3 | import StartButton from './RecordingVariations/Start'; 4 | 5 | const RecordingButton = () => { 6 | const [isRecording, setIsRecording] = useState(true); 7 | const handleClick = () => setIsRecording(!isRecording); 8 | 9 | return ( 10 |
19 | {isRecording ? ( 20 | 21 | ) : ( 22 | 23 | )} 24 |
25 | ); 26 | }; 27 | 28 | export default RecordingButton; 29 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoQuickCheck.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue } from 'recoil'; 3 | import Checkbox from '@mui/material/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-zustand-todo/src/components/TodoQuickCheck.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@mui/material/Checkbox'; 3 | import shallow from 'zustand/shallow'; 4 | import useToDoStore from '../store/store'; 5 | import { useEffect } from 'react'; 6 | 7 | const selector = (state) => ({ 8 | setAllComplete: state.setAllComplete, 9 | checkBox: state.checkBox, 10 | setCheckBox: state.setCheckBox, 11 | }); 12 | 13 | const TodoQuickCheck = () => { 14 | const { setAllComplete, checkBox, setCheckBox } = useToDoStore(selector, shallow); 15 | 16 | useEffect(() => setCheckBox()); 17 | 18 | return ( 19 |
20 | setAllComplete()} 26 | /> 27 | All 28 |
29 | ); 30 | }; 31 | 32 | export default TodoQuickCheck; 33 | -------------------------------------------------------------------------------- /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-zustand-todo/src/components/ReadOnlyTodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@mui/material/Checkbox'; 3 | import '../styles/styles.css'; 4 | import useToDoStore from '../store/store'; 5 | 6 | const ReadOnlyTodoItem = ({ item }) => { 7 | const checkBoxClasses = { 8 | low: 'lowPriority', 9 | medium: 'mediumPriority', 10 | high: 'highPriority', 11 | }; 12 | 13 | const todoList = useToDoStore((state) => state.todoListState); 14 | 15 | return todoList.find((todo) => todo.id === item.id) ? ( 16 |
17 | 18 | todo.id === item.id).isComplete} 21 | color="default" 22 | inputProps={{ 'aria-label': 'primary checkbox' }} 23 | style={{ cursor: 'default' }} 24 | /> 25 |
26 | ) : null; 27 | }; 28 | export default ReadOnlyTodoItem; 29 | -------------------------------------------------------------------------------- /demo-todo/src/components/ReadOnlyTodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@mui/material/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 | -------------------------------------------------------------------------------- /demo-zustand-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 | test: /\.(png|jpg)$/, 37 | use: ['file-loader', 'url-loader?limit=8192'], 38 | }, 39 | ], 40 | }, 41 | resolve: { 42 | extensions: ['.js', '.jsx'], 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /jenkins/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo 'The following "npm" command (if executed) installs the "cross-env"' 4 | echo 'dependency into the local "node_modules" directory, which will ultimately' 5 | echo 'be stored in the Jenkins home directory. As described in' 6 | echo 'https://docs.npmjs.com/cli/install, the "--save-dev" flag causes the' 7 | echo '"cross-env" dependency to be installed as "devDependencies". For the' 8 | echo 'purposes of this tutorial, this flag is not important. However, when' 9 | echo 'installing this dependency, it would typically be done so using this' 10 | echo 'flag. For a comprehensive explanation about "devDependencies", see' 11 | echo 'https://stackoverflow.com/questions/18875674/whats-the-difference-between-dependencies-devdependencies-and-peerdependencies.' 12 | set -x 13 | npm install --save-dev cross-env 14 | set +x 15 | 16 | echo 'The following "npm" command tests that your simple Node.js/React' 17 | echo 'application renders satisfactorily. This command actually invokes the test' 18 | echo 'runner Jest (https://facebook.github.io/jest/).' 19 | set -x 20 | npm --prefix ./package run test 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo-zustand-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/zustand_generator/__tests__/api.test.js: -------------------------------------------------------------------------------- 1 | import { ledger } from '../src/utils/ledger'; 2 | import { chromogenZustandMiddleware } from '../src/api/api'; 3 | import { renderHook, act } from '@testing-library/react'; 4 | import create from 'zustand'; 5 | 6 | // testing chromogenZustandMiddleware 7 | describe('chromogenZustandMiddleware', () => { 8 | // destructuring atoms from ledger interface in utils folder 9 | it('is a function', () => { 10 | expect(typeof chromogenZustandMiddleware).toBe('function'); 11 | }); 12 | 13 | it('should update ledger upon invocation', () => { 14 | // creating a mock store 15 | const useStore = create( 16 | chromogenZustandMiddleware((set) => ({ 17 | count: 0, 18 | increment: () => { 19 | set((state) => ({ count: count + 1 }), false, 'increment'); 20 | }, 21 | })), 22 | ); 23 | //rendering the useStore Hook 24 | const { result } = renderHook(useStore); 25 | 26 | // verifying atoms property (array) on ledger has been updated with input atom 27 | expect(result.current.count).toStrictEqual(0); 28 | expect(ledger.initialRender).toStrictEqual({ count: 0 }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/Quotes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import shallow from 'zustand/shallow'; 3 | import useToDoStore from '../store/store'; 4 | import { useEffect } from 'react'; 5 | 6 | const selector = (state) => ({ 7 | changeQuoteText: state.changeQuoteText, 8 | quoteText: state.quoteText, 9 | }); 10 | 11 | const Quotes = () => { 12 | const { changeQuoteText, quoteText } = useToDoStore(selector, shallow); 13 | 14 | const fetchMe = () => { 15 | let randomNum = Math.floor(Math.random() * 1643); 16 | 17 | fetch('https://type.fit/api/quotes') 18 | .then((response) => response.json()) 19 | .then((data) => { 20 | const quote = data[randomNum]; 21 | changeQuoteText(`"${quote.text}"\n\t- ${quote.author || 'unknown'}`); 22 | }) 23 | .catch((err) => { 24 | console.error(err); 25 | return 'No quote available'; 26 | }); 27 | }; 28 | 29 | useEffect(() => fetchMe(), []); 30 | 31 | return ( 32 | <> 33 |
34 |

{quoteText}

35 | fetchMe()}>New Quote 36 |
37 | 38 | ); 39 | }; 40 | 41 | export default Quotes; 42 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Numbers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const listStyle: React.CSSProperties = { 4 | paddingBlock: '28px', 5 | display: 'flex', 6 | flexDirection: 'column', 7 | alignContent: 'flex-end', 8 | textAlign: 'end', 9 | background: '#1c1c1c', 10 | paddingInline: '16px', 11 | border: '1px solid #1c1c1c', 12 | height: 'auto', 13 | width: '60px', 14 | }; 15 | 16 | const numberStyle: React.CSSProperties = { 17 | fontSize: 12, 18 | color: '#747478', 19 | fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo,monospace', 20 | }; 21 | 22 | const unique = (val: number | string) => ( 23 |

24 | {val} 25 |

26 | ); 27 | 28 | const numerous = (num = 1000) => { 29 | let pointer: number = 1; 30 | let allNumbers: JSX.Element[] = []; 31 | 32 | while (pointer < num) { 33 | allNumbers = [...allNumbers, unique(pointer)]; 34 | pointer++; 35 | } 36 | 37 | console.log(allNumbers); 38 | 39 | return allNumbers; 40 | }; 41 | 42 | const NumberList = ({ number }): JSX.Element =>
{numerous(number)}
; 43 | 44 | export default NumberList; 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /jenkins/scripts/deliver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo 'The following "npm" command builds your Node.js/React application for' 4 | echo 'production in the local "build" directory (i.e. within the' 5 | echo '"/var/jenkins_home/workspace/simple-node-js-react-app" directory),' 6 | echo 'correctly bundles React in production mode and optimizes the build for' 7 | echo 'the best performance.' 8 | set -x 9 | npm run build 10 | set +x 11 | 12 | echo 'The following "npm" command runs your Node.js/React application in' 13 | echo 'development mode and makes the application available for web browsing.' 14 | echo 'The "npm start" command has a trailing ampersand so that the command runs' 15 | echo 'as a background process (i.e. asynchronously). Otherwise, this command' 16 | echo 'can pause running builds of CI/CD applications indefinitely. "npm start"' 17 | echo 'is followed by another command that retrieves the process ID (PID) value' 18 | echo 'of the previously run process (i.e. "npm start") and writes this value to' 19 | echo 'the file ".pidfile".' 20 | set -x 21 | npm --prefix ./package run symlink & 22 | sleep 1 23 | echo $! > .pidfile 24 | set +x 25 | 26 | echo 'Now...' 27 | echo 'Visit http://localhost:3003 to see your Node.js/React application in action.' 28 | echo '(This is why you specified the "args ''-p 3003:3003''" parameter when you' 29 | echo 'created your initial Pipeline as a Jenkinsfile..)' 30 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/ChromogenZustandObserver.tsx: -------------------------------------------------------------------------------- 1 | import Editor from './Editor'; 2 | import EditorTab from './EditorTab'; 3 | import React, { useState } from 'react'; 4 | import { generateTests } from './component-utils'; 5 | import GlobalStyle from '../GlobalStyle'; 6 | 7 | const panel: React.CSSProperties = { 8 | display: 'flex', 9 | position: 'relative', 10 | // width: '531.49px' 11 | }; 12 | 13 | interface Props { 14 | children: JSX.Element; 15 | } 16 | 17 | export const ChromogenZustandObserver: React.FC = ({ children }): JSX.Element => { 18 | const [code, setCode] = React.useState(''); 19 | const [storeMap] = React.useState>(new Map()); 20 | const [isHidden, setIsHidden] = useState(false); 21 | 22 | const timer = setInterval(() => { 23 | console.log('Firing'); 24 | setCode(String(generateTests(storeMap))); 25 | }, 1000); 26 | 27 | React.useEffect(() => { 28 | console.log(code); 29 | timer; 30 | }, [timer]); 31 | 32 | // React.useEffect(() => console.log(ledger.transactions[2].changedValues), []); 33 | 34 | return ( 35 |
36 | {children} 37 | {isHidden ? ( 38 | 39 | ) : ( 40 | 41 | )} 42 | 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@mui/material/Checkbox'; 3 | import '../styles/styles.css'; 4 | import shallow from 'zustand/shallow'; 5 | import useToDoStore from '../store/store'; 6 | 7 | const selector = (state) => ({ 8 | todoListState: state.todoListState, 9 | deleteTodoListItem: state.deleteTodoListItem, 10 | editItemText: state.editItemText, 11 | toggleItemCompletion: state.toggleItemCompletion, 12 | }); 13 | 14 | const TodoItem = ({ item }) => { 15 | const { deleteTodoListItem, editItemText, toggleItemCompletion } = useToDoStore( 16 | selector, 17 | shallow, 18 | ); 19 | 20 | const checkBoxClasses = { 21 | low: 'lowPriority', 22 | medium: 'mediumPriority', 23 | high: 'highPriority', 24 | }; 25 | 26 | return ( 27 |
28 | editItemText(e.target.value, item.id)} 32 | /> 33 | toggleItemCompletion(item.id)} 39 | /> 40 | 43 |
44 | ); 45 | }; 46 | export default TodoItem; 47 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Resizing/Resizer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const ResizerStyle = styled.div` 5 | position: absolute; 6 | cursor: ew-resize; 7 | width: 2px; 8 | height: 100%; 9 | z-index: 1; 10 | left: -1; 11 | top: 0; 12 | &:hover { 13 | background: #4848be; 14 | } 15 | `; 16 | 17 | interface ResizerProps { 18 | onResize: Function; 19 | } 20 | 21 | const Resizer: React.FC = ({ onResize }) => { 22 | const [direction, setDirection] = useState(''); 23 | const [mouseDown, setMouseDown] = useState(false); 24 | 25 | useEffect(() => { 26 | const handleMouseMove = (e) => { 27 | if (!direction) return; 28 | onResize(direction, e.movementX, e.movementY); 29 | }; 30 | 31 | if (mouseDown) { 32 | window.addEventListener('mousemove', handleMouseMove); 33 | } 34 | 35 | return () => window.removeEventListener('mousemove', handleMouseMove); 36 | }, [mouseDown, direction, onResize]); 37 | 38 | useEffect(() => { 39 | const handleMouseUp = () => setMouseDown(false); 40 | window.addEventListener('mouseup', handleMouseUp); 41 | 42 | return () => window.removeEventListener('mouseup', handleMouseUp); 43 | }, []); 44 | 45 | const handleMouseDown = (direction) => { 46 | setDirection(direction); 47 | setMouseDown(true); 48 | }; 49 | 50 | return handleMouseDown('left')}>; 51 | }; 52 | 53 | export default Resizer; 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package/zustand_generator/src/output/output.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Ledger } from '../types'; 3 | import { importZustandStore, testInitialState, testStateChangesAct } from './output-utils'; 4 | /* eslint-enable */ 5 | 6 | /* ----- MAIN ----- */ 7 | /* Output takes in initialRender and transactions from the ledger and tests them from the functions in output-utils*/ 8 | export const output = ({ initialRender, transactions }: Ledger): string => 9 | ` 10 | import { renderHook, act } from '@testing-library/react'; 11 | ${importZustandStore()} 12 | 13 | describe('INITIAL RENDER', () => { 14 | const { result } = renderHook(useStore); 15 | 16 | ${testInitialState(initialRender)} 17 | }); 18 | 19 | 20 | describe('STATE CHANGES', () => { 21 | const { result } = renderHook(useStore); 22 | 23 | ${testStateChangesAct(transactions)} 24 | });`; 25 | 26 | export const unitOutput = (initialRender: any, action: any): string => { 27 | console.log('within unitOutput. init, action : ', initialRender, action); 28 | let retString = ''; 29 | if (initialRender) { 30 | console.log('within unitOutput initialRender'); 31 | retString += ` 32 | import { renderHook, act } from '@testing-library/react'; 33 | ${importZustandStore()} 34 | describe('INITIAL RENDER', () => { 35 | const { result } = renderHook(useStore); 36 | ${testInitialState(initialRender)} 37 | }); 38 | `; 39 | } else if (action) { 40 | console.log('within unitOutput action'); 41 | retString += ` 42 | describe('STATE CHANGES', () => { 43 | const { result } = renderHook(useStore); 44 | ${testStateChangesAct([action])} 45 | });`; 46 | } 47 | return retString; 48 | }; 49 | 50 | //NOTE: Test output is not linted/formatted in any meaningful way. The Chromogen team recommends formatting tests in line with your personal or organizational preferences; 51 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import useToDoStore from '../store/store'; 3 | import shallow from 'zustand/shallow'; 4 | 5 | import ReadOnlyTodoItem from './ReadOnlyTodoItem'; 6 | 7 | const selector = (state) => ({ 8 | searchResultState: state.searchResultState, 9 | setSearchState: state.setSearchState, 10 | }); 11 | 12 | const SearchBar = () => { 13 | const [searchFilter, setSearchFilter] = useState('all'); 14 | const [searchText, setSearchText] = useState(''); 15 | const { searchResultState, setSearchState } = useToDoStore(selector, shallow); 16 | const searchResults = searchResultState[searchFilter]; 17 | 18 | const onSearchTextChange = (e) => { 19 | setSearchText(e.target.value); 20 | setSearchState(e.target.value, searchFilter); 21 | }; 22 | const onSelectChange = (e) => { 23 | setSearchText(''); 24 | setSearchFilter(e.target.value); 25 | }; 26 | 27 | return ( 28 |
29 | 37 | 47 |
48 | {searchResults.results.map((result, idx) => ( 49 | 50 | ))} 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default SearchBar; 57 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import Checkbox from '@mui/material/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/zustand_generator/src/component/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import CodeEditor from '@uiw/react-textarea-code-editor'; 3 | import NumberList from './Numbers'; 4 | import { Header } from './Header'; 5 | import RecordingButton from './Buttons/RecordingButton'; 6 | 7 | const editorStyle: React.CSSProperties = { 8 | display: 'flex', 9 | flexDirection: 'column', 10 | height: '100%', 11 | overflow: 'auto', 12 | borderLeft: '1px solid rgba(243,246,248,.1)', 13 | backgroundColor: '#1C1C1C', 14 | width: '50vw', 15 | }; 16 | const codePanel: React.CSSProperties = { 17 | display: 'flex', 18 | // flexGrow: 1, 19 | overflowY: 'scroll', 20 | height: 'calc(100vh - 56px)', 21 | }; 22 | 23 | interface Props { 24 | code: string; 25 | setIsHidden: Function; 26 | isHidden: boolean; 27 | } 28 | const Editorfield = ({ code, isHidden, setIsHidden }: Props): JSX.Element => { 29 | const [, setInnerCode] = useState(code); 30 | let breakLine = 0; 31 | 32 | for (let curr = 0; curr < code.length; curr++) { 33 | if (code[curr] == '\n') breakLine++; 34 | } 35 | 36 | console.log(breakLine); 37 | 38 | return ( 39 |
40 |
41 |
42 | 43 | setInnerCode(evn.target.value)} 49 | padding={15} 50 | style={{ 51 | maxWidth: 1000, 52 | width: 'calc(100% - 60px)', 53 | maxHeight: '100vh', 54 | overflow: 'visible', 55 | fontSize: 12, 56 | backgroundColor: '#1c1c1c', 57 | fontFamily: 58 | 'ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo,monospace', 59 | }} 60 | /> 61 |
62 | 63 |
64 | ); 65 | }; 66 | 67 | export default Editorfield; 68 | -------------------------------------------------------------------------------- /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": "NODE_OPTIONS=--experimental-vm-modules npx 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.19.1", 45 | "@babel/preset-react": "^7.10.4", 46 | "@testing-library/react": "^13.1.1", 47 | "babel-loader": "^8.1.0", 48 | "chromogen": "^4.0.4", 49 | "css-loader": "^4.2.1", 50 | "identity-obj-proxy": "^3.0.0", 51 | "jest": "^26.4.2", 52 | "style-loader": "^1.2.1", 53 | "webpack": "^5.74.0", 54 | "webpack-cli": "^3.3.12", 55 | "webpack-dev-server": "^4.11.1" 56 | }, 57 | "peerDependencies": { 58 | "typescript": "^4.0.3" 59 | }, 60 | "dependencies": { 61 | "@babel/runtime": "^7.11.2", 62 | "@emotion/react": "^11.10.4", 63 | "@emotion/styled": "^11.10.4", 64 | "@mui/icons-material": "^5.10.6", 65 | "@mui/material": "^5.10.6", 66 | "babel-jest": "^26.3.0", 67 | "react": "^18.0.0", 68 | "react-dom": "^18.0.0", 69 | "react-recoil-hooks-testing-library": "^0.1.0", 70 | "react-test-renderer": "^18.1.0", 71 | "recoil": "0.7.2", 72 | "typescript": "^4.0.3" 73 | }, 74 | "jest": { 75 | "moduleNameMapper": { 76 | "\\.(css|less)$": "identity-obj-proxy" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6" /* 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": false /* 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 | // "include": ["package/**/*"] 27 | } -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Buttons/RecordingVariations/Start.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const Start = (props): JSX.Element => { 4 | //hover 5 | const [isHover, setIsHover] = useState(false); 6 | const handleMouseEnter = () => { 7 | setIsHover(true); 8 | }; 9 | const handleMouseLeave = () => { 10 | setIsHover(false); 11 | }; 12 | 13 | const startButtonShape: React.CSSProperties = { 14 | display: 'flex', 15 | width: '252px', 16 | flexDirection: 'row', 17 | alignItems: 'center', 18 | position: 'absolute', 19 | justifyContent: 'center', 20 | height: '48px', 21 | // left: '1482px', 22 | // top: '1081px', 23 | borderRadius: '42px', 24 | padding: '14px 24px', 25 | columnGap: '16px', 26 | background: '#212121', 27 | border: '1px solid rgba(243, 246, 248, 0.1)', 28 | cursor: 'pointer', 29 | bottom: '20px', 30 | }; 31 | 32 | const startButtonHover: React.CSSProperties = { 33 | display: 'flex', 34 | flexDirection: 'row', 35 | alignItems: 'center', 36 | position: 'absolute', 37 | width: '252px', 38 | height: '48px', 39 | borderRadius: '42px', 40 | padding: '14px 24px', 41 | columnGap: '16px', 42 | justifyContent: 'center', 43 | background: '#1C1C1C', 44 | border: '1px solid rgba(243, 246, 248, 0.1)', 45 | cursor: 'pointer', 46 | bottom: '20px', 47 | }; 48 | 49 | const startIcon: React.CSSProperties = { 50 | width: '0', 51 | height: '0', 52 | borderTop: '8px solid transparent', 53 | borderBottom: '8px solid transparent', 54 | borderLeft: '16px solid rgba(243, 246, 248, 0.8)', 55 | flex: 'none', 56 | order: '0', 57 | flexGrow: '0', 58 | borderRadius: '2px', 59 | }; 60 | 61 | const startText: React.CSSProperties = { 62 | fontSize: '14px', 63 | lineHeight: '16px', 64 | color: '#F3F6F8', 65 | opacity: '0.8', 66 | flex: 'none', 67 | order: '1', 68 | flexGrow: '0', 69 | }; 70 | 71 | return ( 72 | 81 | ); 82 | }; 83 | 84 | export default Start; 85 | -------------------------------------------------------------------------------- /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/zustand_generator/src/component/component-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { CSSProperties } from 'react'; 3 | // import type { SerializableParam } from 'recoil'; 4 | import type { Ledger } from '../types'; 5 | 6 | import { ledger } from '../utils/ledger'; 7 | import { output } from '../output/output'; 8 | /* eslint-enable */ 9 | 10 | const buttonStyle: CSSProperties = { 11 | display: 'inline-block', 12 | margin: '8px', 13 | marginLeft: '13px', 14 | padding: '0px', 15 | height: '25px', 16 | width: '65px', 17 | borderRadius: '4px', 18 | justifyContent: 'space-evenly', 19 | border: '1px', 20 | cursor: 'pointer', 21 | color: '#90d1f0', 22 | fontSize: '10px', 23 | }; 24 | 25 | const divStyle: CSSProperties = { 26 | display: 'flex', 27 | position: 'relative', 28 | height: '100%', 29 | top: '0px', 30 | right: '0px', 31 | bottom: '0px', 32 | width: '30vw', 33 | backgroundColor: '#222222', 34 | borderRadius: '4px', 35 | margin: 0, 36 | padding: 0, 37 | zIndex: 999999, 38 | }; 39 | 40 | const playStyle: CSSProperties = { 41 | boxSizing: 'border-box', 42 | marginLeft: '25px', 43 | borderStyle: 'solid', 44 | borderWidth: '7px 0px 7px 14px', 45 | }; 46 | 47 | const pauseStyle: CSSProperties = { 48 | width: '14px', 49 | height: '14px', 50 | borderWidth: '0px 0px 0px 10px', 51 | borderStyle: 'double', 52 | marginLeft: '27px', 53 | }; 54 | 55 | export const styles = { buttonStyle, divStyle, playStyle, pauseStyle }; 56 | 57 | /* 58 | generateFile generates test file & sets download URL 59 | The passed in setFile function updates _file_ state in Chromogen observer 60 | Applying only at point-of-download keeps performance cost low for users who 61 | don't need to pass nodes while creating a moderate performance hit for others 62 | only while downloading, never while interacting with their app. 63 | */ 64 | export const generateFile = (setFile: Function, storeMap: Map): string[] => { 65 | const tests = generateTests(storeMap); 66 | const blob = new Blob(tests); 67 | setFile(URL.createObjectURL(blob)); 68 | return tests; 69 | }; 70 | 71 | /* generateTests is invoked within generateFile, returning our desired test string within an array */ 72 | export const generateTests = (storeMap: Map): string[] => { 73 | const { initialRender, transactions } = ledger; 74 | 75 | const finalLedger: Ledger = 76 | storeMap.size > 0 77 | ? { 78 | initialRender, 79 | transactions: transactions, 80 | } 81 | : { ...ledger }; 82 | 83 | return [output(finalLedger)]; 84 | }; 85 | -------------------------------------------------------------------------------- /package/zustand_generator/src/GlobalStyle.ts: -------------------------------------------------------------------------------- 1 | /* Reset CSS section */ 2 | import { createGlobalStyle } from 'styled-components'; 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | *, 6 | *::before, 7 | *::after { 8 | box-sizing: border-box; 9 | } 10 | 11 | * { 12 | margin: 0; 13 | } 14 | 15 | html, 16 | body, 17 | #root // for create-react-app 18 | { 19 | height: 100%; 20 | } 21 | 22 | img, 23 | picture, 24 | video, 25 | canvas, 26 | svg { 27 | display: block; 28 | max-width: 100%; 29 | } 30 | 31 | input, 32 | button, 33 | textarea, 34 | select { 35 | font: inherit; 36 | } 37 | 38 | p, 39 | h1, 40 | h2, 41 | h3, 42 | h4, 43 | h5, 44 | h6 { 45 | overflow-wrap: break-word; 46 | } 47 | 48 | h2 { 49 | font-size: 36px; 50 | } 51 | 52 | #root, 53 | #__next { 54 | isolation: isolate; 55 | } 56 | 57 | @media (prefers-color-scheme: dark) { 58 | :root { 59 | --background-default: #222; 60 | --background-body: #161616; 61 | --background-button: #dbdbdb; 62 | --background-button-hover: #fafafa; 63 | --card-background: #323232; 64 | --card-border: #000; 65 | --input-background: #333; 66 | --input-border: #222; 67 | --input-border-hover: #222; 68 | --font-default: #ffffff; 69 | --font-secondary: #8e8e8e; 70 | --font-tertiary: #666; 71 | --font-button: #161616; 72 | --font-link: rgb(0, 149, 246); 73 | --font-error: rgba(220, 70, 70, .6); 74 | --background-error: rgba(220, 70, 70, .3); 75 | } 76 | } 77 | 78 | @media (prefers-color-scheme: light) { 79 | :root { 80 | --background-default: #fff; 81 | --background-body: #fafafa; 82 | --background-button: #323232; 83 | --background-button-hover: #161616; 84 | --card-background: #fff; 85 | --card-border: #dbdbdb; 86 | --input-background: #eee; 87 | --input-border: #dbdbdb; 88 | --input-border-hover: #191919c2; 89 | --font-default: #191919; 90 | --font-secondary: #8e8e8e; 91 | --font-tertiary: #8e8e8e; 92 | --font-button: #fafafa; 93 | --font-link: rgb(0, 149, 246); 94 | --font-error: rgba(220, 70, 70, .6); 95 | --background-error: rgba(220, 70, 70, .3); 96 | } 97 | } 98 | 99 | body { 100 | margin: 0; 101 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 102 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 103 | sans-serif; 104 | box-sizing: border-box; 105 | } 106 | 107 | #app { 108 | height: 100%; 109 | width: 100%; 110 | } 111 | `; 112 | 113 | export default GlobalStyle; 114 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoItem from './TodoItem'; 3 | import TodoItemCreator from './TodoItemCreator'; 4 | import TodoListFilters from './TodoListFilters'; 5 | import TodoQuickCheck from './TodoQuickCheck'; 6 | import Quotes from './Quotes'; 7 | import SearchBar from './SearchBar'; 8 | import '../styles/styles.css'; 9 | import shallow from 'zustand/shallow'; 10 | import useToDoStore from '../store/store'; 11 | 12 | const selector = (state) => ({ 13 | todoListState: state.todoListState, 14 | todoListFilterState: state.todoListFilterState, 15 | todoListSortState: state.todoListSortState, 16 | }); 17 | 18 | const filterList = (list, filter) => { 19 | switch (filter) { 20 | case 'Show Completed': 21 | return list.filter((item) => item.isComplete); 22 | case 'Show Uncompleted': 23 | return list.filter((item) => !item.isComplete); 24 | default: 25 | return list; 26 | } 27 | }; 28 | 29 | const sortList = (list, sortingMethod) => { 30 | if (!sortingMethod) return list; 31 | const high = list.filter((item) => item.priority === 'high'); 32 | const medium = list.filter((item) => item.priority === 'medium'); 33 | const low = list.filter((item) => item.priority === 'low'); 34 | return [...high, ...medium, ...low]; 35 | }; 36 | 37 | const TodoList = () => { 38 | const { todoListState, todoListFilterState, todoListSortState } = useToDoStore(selector, shallow); 39 | const todoList = sortList(filterList(todoListState, todoListFilterState), todoListSortState); 40 | 41 | return ( 42 |
43 |
44 |
45 | 50 |
51 |
52 | Loading...}> 53 | 54 | 55 |
56 |
57 |

To-Do List

58 |
59 | 60 | 61 | {todoList.map((todoItem) => ( 62 | 63 | ))} 64 | 65 |
66 |
67 | 68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export default TodoList; 75 | -------------------------------------------------------------------------------- /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(selectors) 42 | + importRecoilFamily(selectorFamilies) 43 | } 44 | } from ''; 45 | import { 46 | ${ 47 | importRecoilState(atoms) 48 | + importRecoilFamily(atomFamilies) 49 | } 50 | } from ''; 51 | 52 | // Suppress 'Batcher' warnings from React / Recoil conflict 53 | console.error = jest.fn(); 54 | 55 | // Hook to return atom/selector values and/or modifiers for react-recoil-hooks-testing-library 56 | const useStoreHook = () => { 57 | // atoms 58 | ${writeableHook(atoms)} 59 | // writeable selectors 60 | ${writeableHook(setters)} 61 | // read-only selectors 62 | ${readableHook(setFilter(selectors, setters))} 63 | // atom families 64 | ${atomFamilyHook(transactions)} 65 | // writeable selector families 66 | ${selectorFamilyHook(selectorFamilies, true)} 67 | // read-only selector families 68 | ${selectorFamilyHook(selectorFamilies, false)} 69 | 70 | 71 | 72 | return { 73 | ${ 74 | returnWriteable(atoms) 75 | + returnWriteable(setters) 76 | + returnReadable(setFilter(selectors, setters)) 77 | + returnAtomFamily(transactions) 78 | + returnSelectorFamily(selectorFamilies, true) 79 | + returnSelectorFamily(selectorFamilies, false) 80 | }\t}; 81 | }; 82 | 83 | describe('INITIAL RENDER', () => { 84 | const { result } = renderRecoilHook(useStoreHook); 85 | 86 | ${initializeSelectors(initialRender)} 87 | }); 88 | 89 | describe('SELECTORS', () => { 90 | ${testSelectors(transactions)}}); 91 | 92 | describe('SETTERS', () => { 93 | ${testSetters(setTransactions)}});`; 94 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoItemCreator.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React, { useState } from 'react'; 3 | import RadioGroup from '@mui/material/RadioGroup'; 4 | import FormControlLabel from '@mui/material/FormControlLabel'; 5 | import FormControl from '@mui/material/FormControl'; 6 | import FormLabel from '@mui/material/FormLabel'; 7 | import Radio from '@mui/material/Radio'; 8 | import useToDoStore from '../store/store'; 9 | 10 | const selector = (state) => state.addTodoListItem; 11 | 12 | // utility for creating unique Id 13 | let id = 1; 14 | const getId = () => { 15 | id += 1; 16 | return id; 17 | }; 18 | 19 | const TodoItemCreator = () => { 20 | const [inputValue, setInputValue] = useState(''); 21 | const [priorityValue, setPriorityValue] = useState('low'); 22 | const addTodoListItem = useToDoStore(selector); 23 | 24 | const addItem = () => { 25 | addTodoListItem({ 26 | id: getId(), 27 | text: inputValue, 28 | priority: priorityValue, 29 | isComplete: false, 30 | }); 31 | setInputValue(''); 32 | setPriorityValue('low'); 33 | }; 34 | 35 | const onChange = ({ target: { value } }) => { 36 | setInputValue(value); 37 | }; 38 | 39 | const handleChange = (event) => { 40 | setPriorityValue(event.target.value); 41 | }; 42 | 43 | /* MUI Radio Button styles */ 44 | const GreenRadio = (props) => ; 45 | 46 | const YellowRadio = (props) => ; 47 | 48 | const RedRadio = (props) => ; 49 | 50 | return ( 51 |
52 | 59 | 60 | 61 | 62 | 69 | } value="high" /> 70 | } value="medium" /> 71 | } value="low" /> 72 | 73 | 74 | 75 | 76 | 79 |
80 | ); 81 | }; 82 | 83 | export default TodoItemCreator; 84 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoItemCreator.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React, { useState } from 'react'; 3 | import RadioGroup from '@mui/material/RadioGroup'; 4 | import FormControlLabel from '@mui/material/FormControlLabel'; 5 | import FormControl from '@mui/material/FormControl'; 6 | import FormLabel from '@mui/material/FormLabel'; 7 | import Radio from '@mui/material/Radio'; 8 | import { useSetRecoilState } from 'recoil'; 9 | import { todoListState } from '../store/atoms'; 10 | 11 | // utility for creating unique Id 12 | let id = 0; 13 | const getId = () => { 14 | id += 1; 15 | return id; 16 | }; 17 | 18 | const TodoItemCreator = () => { 19 | const [inputValue, setInputValue] = useState(''); 20 | const [priorityValue, setPriorityValue] = useState('low'); 21 | const setTodoList = useSetRecoilState(todoListState); 22 | 23 | const addItem = () => { 24 | setTodoList((oldTodoList) => [ 25 | ...oldTodoList, 26 | { 27 | id: getId(), 28 | text: inputValue, 29 | priority: priorityValue, 30 | isComplete: false, 31 | }, 32 | ]); 33 | setInputValue(''); 34 | setPriorityValue('low'); 35 | }; 36 | 37 | const onChange = ({ target: { value } }) => { 38 | setInputValue(value); 39 | }; 40 | 41 | const handleChange = (event) => { 42 | setPriorityValue(event.target.value); 43 | }; 44 | 45 | /* MUI Radio Button styles */ 46 | const GreenRadio = (props) => ; 47 | 48 | const YellowRadio = (props) => ; 49 | 50 | const RedRadio = (props) => ; 51 | 52 | return ( 53 |
54 | 61 | 62 | 63 | 64 | 71 | } value="high" /> 72 | } value="medium" /> 73 | } value="low" /> 74 | 75 | 76 | 77 | 78 | 81 |
82 | ); 83 | }; 84 | 85 | export default TodoItemCreator; 86 | -------------------------------------------------------------------------------- /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 | 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 | 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 | 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 | 67 | export {debouncedAddToTransactions, wrapGetter, wrapSetter}; -------------------------------------------------------------------------------- /demo-zustand-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen-todo", 3 | "version": "1.0.2", 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 | "update": " npm run uninstall && npm run install && npm run start", 10 | "uninstall": "npm uninstall chromogen", 11 | "install": "npm install ../package", 12 | "buildPackage": "tsc", 13 | "tarballUpdate": "npm --prefix ../package run build && npm pack ../package && npm uninstall chromogen && npm install ./chromogen-5.0.1.tgz && npm start" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "recoil", 18 | "chromogen", 19 | "demo", 20 | "example", 21 | "todo" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/open-source-labs/Chromogen.git" 26 | }, 27 | "contributors": [ 28 | { 29 | "name": "Brach Burdick", 30 | "url": "https://github.com/sirbrachthepale/" 31 | }, 32 | { 33 | "name": "Francois Denavaut", 34 | "url": "https://github.com/dnvt/" 35 | }, 36 | { 37 | "name": "Maggie Kwan", 38 | "url": "https://github.com/maggiekwan/" 39 | }, 40 | { 41 | "name": "Lawrence Liang", 42 | "url": "https://github.com/Lawliang/" 43 | }, 44 | { 45 | "name": "Michelle Holland", 46 | "url": "https://github.com/michellebholland/" 47 | }, 48 | { 49 | "name": "Jim Chen", 50 | "url": "https://github.com/chenchingk" 51 | }, 52 | { 53 | "name": "Andy Wang", 54 | "url": "https://github.com/andywang23" 55 | }, 56 | { 57 | "name": "Connor Rose Delisle", 58 | "url": "https://github.com/connorrose" 59 | } 60 | ], 61 | "license": "MIT", 62 | "devDependencies": { 63 | "@babel/core": "^7.11.1", 64 | "@babel/plugin-transform-runtime": "^7.11.0", 65 | "@babel/preset-env": "^7.11.0", 66 | "@babel/preset-react": "^7.10.4", 67 | "@testing-library/react": "^13.1.1", 68 | "babel-loader": "^8.1.0", 69 | "css-loader": "^4.2.1", 70 | "identity-obj-proxy": "^3.0.0", 71 | "jest": "^26.4.2", 72 | "prettier": "^2.7.1", 73 | "style-loader": "^1.2.1", 74 | "webpack": "^4.44.1", 75 | "webpack-cli": "^3.3.12", 76 | "webpack-dev-server": "^3.11.0" 77 | }, 78 | "peerDependencies": { 79 | "typescript": "^4.0.3" 80 | }, 81 | "dependencies": { 82 | "@babel/runtime": "^7.11.2", 83 | "@emotion/react": "^11.10.5", 84 | "@emotion/styled": "^11.10.4", 85 | "@mui/icons-material": "^5.10.6", 86 | "@mui/material": "^5.10.6", 87 | "babel-jest": "^26.3.0", 88 | "chromogen": "file:chromogen-4.0.4.tgz", 89 | "file-loader": "^6.2.0", 90 | "react": "^18.0.0", 91 | "react-dom": "^18.0.0", 92 | "react-test-renderer": "^18.1.0", 93 | "recoil": "0.7.5", 94 | "typescript": "^4.0.3", 95 | "url-loader": "^4.1.1", 96 | "zustand": "^4.1.1" 97 | }, 98 | "jest": { 99 | "moduleNameMapper": { 100 | "\\.(css|less)$": "identity-obj-proxy" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /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 | xit('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 '@mui/icons-material/Sort'; 3 | import EqualizerIcon from '@mui/icons-material/Equalizer'; 4 | import RefreshIcon from '@mui/icons-material/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: 'aqua', 33 | }; 34 | 35 | return ( 36 |
    37 | 47 | 57 | 66 | 69 | 70 | 81 | 84 |
85 | ); 86 | }; 87 | 88 | export default TodoListFilters; 89 | -------------------------------------------------------------------------------- /.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 | // added below for npm install -D eslint-config-airbnb-typescript --legacy-peer-deps 15 | "airbnb-typescript", 16 | "prettier", 17 | "prettier/react" 18 | ], 19 | "plugins": ["prettier"], 20 | "parserOptions": { 21 | "ecmaVersion": 2022, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | }, 26 | "project": "./package/tsconfig.json", 27 | "tsconfigRootDir": "__dirname" 28 | }, 29 | "settings": { 30 | "import/extensions": [ 31 | ".js", 32 | ".jsx", 33 | ".ts", 34 | ".tsx" 35 | ], 36 | "import/parsers": { 37 | "@typescript-eslint/parser": [ 38 | ".ts", 39 | ".tsx" 40 | ] 41 | }, 42 | "import/resolver": { 43 | "typescript": { 44 | "directory": "./package/tsconfig.json" 45 | }, 46 | "node": { 47 | "extensions": [ 48 | ".js", 49 | ".jsx", 50 | ".ts", 51 | ".tsx" 52 | ] 53 | } 54 | }, 55 | "rules": { 56 | "prettier/prettier": ["warn"], 57 | "arrow-body-style": ["error", "as-needed"], 58 | "default-case-last": "error", 59 | "default-param-last": ["error"], 60 | "func-style": ["off", "expression"], 61 | "no-constant-condition": "error", 62 | "no-useless-call": "error", 63 | "prefer-exponentiation-operator": "error", 64 | "prefer-regex-literals": "error", 65 | "quotes": [ 66 | "error", 67 | "single", 68 | { 69 | "avoidEscape": true, 70 | "allowTemplateLiterals": false 71 | } 72 | ], 73 | "import/prefer-default-export": "off", 74 | "import/extensions": [ 75 | "error", 76 | "ignorePackages", 77 | { 78 | "js": "never", 79 | "jsx": "never", 80 | "ts": "never", 81 | "tsx": "never" 82 | } 83 | ], 84 | "react/jsx-filename-extension": ["off"], 85 | "react/function-component-definition": [ 86 | "error", 87 | { 88 | "namedComponents": "arrow-function", 89 | "unnamedComponents": "arrow-function" 90 | } 91 | ], 92 | "react/jsx-handler-names": [ 93 | "error", 94 | { 95 | "eventHandlerPrefix": "handle", 96 | "eventHandlerPropPrefix": "on" 97 | } 98 | ], 99 | "react/jsx-key": "error", 100 | "react/jsx-no-useless-fragment": "error", 101 | "react/jsx-sort-props": [ 102 | "error", 103 | { 104 | "callbacksLast": true, 105 | "shorthandFirst": true, 106 | "shorthandLast": false, 107 | "ignoreCase": true, 108 | "noSortAlphabetically": false, 109 | "reservedFirst": true 110 | } 111 | ], 112 | "react/no-adjacent-inline-elements": "error", 113 | "react/no-direct-mutation-state": "error", 114 | "react/no-multi-comp": "error", 115 | "react/prop-types": [ 116 | "error", 117 | { "skipUndeclared": true } 118 | ] 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /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 123 | ); 124 | }; 125 | 126 | export default SecondaryButton; 127 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoListFilters.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import SortIcon from '@mui/icons-material/Sort'; 3 | import EqualizerIcon from '@mui/icons-material/Equalizer'; 4 | import RefreshIcon from '@mui/icons-material/Refresh'; 5 | import useToDoStore from '../store/store'; 6 | import shallow from 'zustand/shallow'; 7 | 8 | const selector = (state) => ({ 9 | todoListFilterState: state.todoListFilterState, 10 | todoListState: state.todoListState, 11 | resetFiltersAndSorted: state.resetFiltersAndSorted, 12 | todoListSortState: state.todoListSortState, 13 | toggleSort: state.toggleSort, 14 | setFilter: state.setFilter, 15 | }); 16 | 17 | const TodoListFilters = () => { 18 | const { 19 | todoListFilterState, 20 | todoListState, 21 | resetFiltersAndSorted, 22 | todoListSortState, 23 | toggleSort, 24 | setFilter, 25 | } = useToDoStore(selector, shallow); 26 | 27 | // // selector - grabs totals for each category 28 | const { high, medium, low } = todoListState.reduce((acc, cur) => { 29 | acc[cur.priority] = (acc[cur.priority] ?? 0) + 1; 30 | return acc; 31 | }, {}); 32 | 33 | // // toggle priority stats display 34 | const [displayStats, setDisplayStats] = useState(false); 35 | 36 | // // selector - totals for each filter 37 | const totalNum = todoListState.length; 38 | const totalCompletedNum = todoListState.filter((todo) => todo.isComplete).length; 39 | const totalUncompletedNum = todoListState.filter((todo) => !todo.isComplete).length; 40 | 41 | const updateFilter = ({ target: { value } }) => setFilter(value); 42 | 43 | const toggleDisplayStats = () => setDisplayStats(!displayStats); 44 | const reset = () => { 45 | setDisplayStats(false); // displayStats is local state 46 | resetFiltersAndSorted(); 47 | }; 48 | 49 | const sortIconColor = { 50 | true: 'sortedWhite', 51 | false: 'unsortedGray', 52 | }; 53 | 54 | return ( 55 |
    56 | 66 | 76 | 85 | 88 | 89 | 100 | 103 |
104 | ); 105 | }; 106 | 107 | export default TodoListFilters; 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen-root", 3 | "version": "5.0.1", 4 | "description": "simple, interaction-driven test generator for Recoil and Zustand 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": "Brach Burdick", 16 | "url": "https://github.com/sirbrachthepale/" 17 | }, 18 | { 19 | "name": "Francois Denavaut", 20 | "url": "https://github.com/dnvt/" 21 | }, 22 | { 23 | "name": "Maggie Kwan", 24 | "url": "https://github.com/maggiekwan/" 25 | }, 26 | { 27 | "name": "Lawrence Liang", 28 | "url": "https://github.com/Lawliang/" 29 | }, 30 | { 31 | "name": "Michelle Holland", 32 | "url": "https://github.com/michellebholland/" 33 | }, 34 | { 35 | "name": "Jim Chen", 36 | "url": "https://github.com/chenchingk" 37 | }, 38 | { 39 | "name": "Andy Wang", 40 | "url": "https://github.com/andywang23" 41 | }, 42 | { 43 | "name": "Connor Rose Delisle", 44 | "url": "https://github.com/connorrose" 45 | }, 46 | { 47 | "name": "Amy Yee", 48 | "url": "https://github.com/amyy98" 49 | }, 50 | { 51 | "name": "Cameron Greer", 52 | "url": "https://github.com/cgreer011" 53 | }, 54 | { 55 | "name": "Jinseon Shin", 56 | "url": "https://github.com/wlstjs" 57 | }, 58 | { 59 | "name": "Nicholas Shay", 60 | "url": "https://github.com/nicholasjs" 61 | }, 62 | { 63 | "name": "Ryan Tumel", 64 | "url": "https://github.com/rtumel123" 65 | }, 66 | { 67 | "name": "Marcellies Pettiford", 68 | "url": "https://github.com/mp-04" 69 | }, 70 | { 71 | "name": "Sung Kim", 72 | "url": "https://github.com/smk53664" 73 | }, 74 | { 75 | "name": "Lina Lee", 76 | "url": "https://github.com/lina4lee" 77 | }, 78 | { 79 | "name": "Erica Oh", 80 | "url": "https://github.com/ericaysoh" 81 | }, 82 | { 83 | "name": "Dani Almaraz", 84 | "url": "https://github.com/dtalmaraz" 85 | }, 86 | { 87 | "name": "Craig Boswell", 88 | "url": "https://github.com/crgb0s" 89 | }, 90 | { 91 | "name": "Hussein Ahmed", 92 | "url": "https://github.com/Hali3030" 93 | }, 94 | { 95 | "name": "Ian Kila", 96 | "url": "https://github.com/iannkila" 97 | }, 98 | { 99 | "name": "Yuehao Wong", 100 | "url": "https://github.com/yuehaowong" 101 | } 102 | ], 103 | "license": "MIT", 104 | "bugs": { 105 | "url": "https://github.com/oslabs-beta/Chromogen/issues" 106 | }, 107 | "homepage": "https://github.com/oslabs-beta/Chromogen#readme", 108 | "dependencies": { 109 | "@uiw/react-textarea-code-editor": "^2.1.1", 110 | "dependency-cruiser": "^12.9.0", 111 | "react": "^18.0.0", 112 | "react-dom": "^18.0.0", 113 | "recoil": "^0.7.2", 114 | "redux": "^4.0.5", 115 | "styled-components": "^5.3.6", 116 | "zustand": "^4.1.1" 117 | }, 118 | "devDependencies": { 119 | "@babel/core": "^7.11.6", 120 | "@babel/preset-env": "^7.11.5", 121 | "@babel/preset-react": "^7.10.4", 122 | "@babel/preset-typescript": "^7.10.4", 123 | "@testing-library/react": "^13.1.1", 124 | "@types/node": "^14.11.2", 125 | "@types/react": "^18.0.6", 126 | "@types/react-dom": "^18.0.2", 127 | "@types/styled-components": "^5.1.26", 128 | "babel-jest": "^26.3.0", 129 | "coveralls": "^3.1.0", 130 | "css-loader": "^6.7.3", 131 | "eslint-config-airbnb-typescript": "^17.0.0", 132 | "jest": "^26.4.2", 133 | "react-test-renderer": "^18.0.0", 134 | "typescript": "^4.0.3" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Icons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const icon_download = ( 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | ); 17 | 18 | export const icon_copy = ( 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | ); 32 | 33 | export const icon_retract = ( 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | ); 47 | 48 | export const icon_expand = ( 49 | 50 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | ); 62 | 63 | export const icon_arrow = ( 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | 74 | export const icon_check = ( 75 | 76 | 77 | 78 | 79 | 80 | 85 | 86 | 87 | ); 88 | -------------------------------------------------------------------------------- /package/zustand_generator/src/api/api.ts: -------------------------------------------------------------------------------- 1 | import { ledger } from '../utils/ledger'; 2 | import { Transaction, InitialRender } from '../types'; 3 | import { StoreApi, StateCreator, StoreMutatorIdentifier } from 'zustand'; 4 | 5 | // Referenced Zustand First Party Middleware for Type implementation 6 | // See here: https://github.com/pmndrs/zustand/tree/main/src/middleware 7 | 8 | type Chromogen = < 9 | T extends unknown, 10 | Mps extends [StoreMutatorIdentifier, unknown][] = [], 11 | Mcs extends [StoreMutatorIdentifier, unknown][] = [], 12 | >( 13 | creatorFunction: StateCreator, 14 | ) => StateCreator; 15 | 16 | type ChromogenImpl = ( 17 | creatorFunction: PopArgument>, 18 | ) => PopArgument>; 19 | 20 | type PopArgument unknown> = T extends ( 21 | ...a: [...infer A, infer _] 22 | ) => infer R 23 | ? (...a: A) => R 24 | : never; 25 | 26 | type TakeTwo = T extends [] 27 | ? [undefined, undefined] 28 | : T extends [unknown] 29 | ? [...a0: T, a1: undefined] 30 | : T extends [unknown?] 31 | ? [...a0: T, a1: undefined] 32 | : T extends [unknown, unknown] 33 | ? T 34 | : T extends [unknown, unknown?] 35 | ? T 36 | : T extends [unknown?, unknown?] 37 | ? T 38 | : T extends [infer A0, infer A1, ...unknown[]] 39 | ? [A0, A1] 40 | : T extends [infer A0, (infer A1)?, ...unknown[]] 41 | ? [A0, A1?] 42 | : T extends [(infer A0)?, (infer A1)?, ...unknown[]] 43 | ? [A0?, A1?] 44 | : never; 45 | 46 | type StoreDevtools = S extends { 47 | setState: (...a: infer Sa) => infer Sr; 48 | } 49 | ? { 50 | setState(...a: [...a: TakeTwo, action?: A]): Sr; 51 | } 52 | : never; 53 | 54 | type Write = Omit & U; 55 | 56 | type WithDevtools = Write>; 57 | 58 | type NamedSet = WithDevtools>['setState']; 59 | /* 60 | Chromogen Middleware business logic. Performs 2 main functions: 61 | 1. Captures initial state for all store properties and writes to ledger as initialRender (For generating initial state tests) 62 | 2. Wraps set function to capture any subsequent state changes (along with funciton name, arguments, and before/after for changes store slices) 63 | */ 64 | const chromogenImpl: ChromogenImpl = (creatorFunction) => (set, get, api) => { 65 | //get initial render and save it to ledger 66 | const initialStateEntries = creatorFunction(api.setState, get, api); 67 | const initialRender: InitialRender = filterOutFuncs(initialStateEntries); 68 | ledger.initialRender = initialRender; 69 | 70 | type S = ReturnType; 71 | (api.setState as NamedSet) = (partial, replace, action, ...args) => { 72 | const oldStore = filterOutFuncs(get()); 73 | const r = set(partial, replace); 74 | const newStore = filterOutFuncs(get()); 75 | const changedValues = diffStateObjects(oldStore, newStore); 76 | 77 | //create Transaction obj and write it to ledger for generating tests 78 | const newAction: Transaction = { 79 | action: typeof action === 'string' ? action : 'UnknownAction', 80 | changedValues, 81 | arguments: args, 82 | }; 83 | 84 | ledger.transactions.push(newAction); 85 | return r; 86 | }; 87 | return creatorFunction(api.setState, get, api); 88 | }; 89 | 90 | export const chromogenZustandMiddleware = chromogenImpl as unknown as Chromogen; 91 | 92 | /* Goes through the store object and returns a new object containing state without any actions*/ 93 | const filterOutFuncs = (store) => { 94 | const result = {}; 95 | for (const [k, v] of Object.entries(store)) { 96 | if (typeof v !== 'function') result[k] = v; 97 | } 98 | return result; 99 | }; 100 | /* Identifies the difference between initial Store and newStore containing newly invoked actions */ 101 | const diffStateObjects = (oldStore, newStore) => { 102 | const changedValues = {}; 103 | for (const [k, v] of Object.entries(newStore)) { 104 | if (JSON.stringify(oldStore[k]) !== JSON.stringify(v)) changedValues[k] = v; 105 | } 106 | return changedValues; 107 | }; 108 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Buttons/RecordingVariations/Record.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const Record = (props): JSX.Element => { 4 | //hover 5 | const [isHover, setIsHover] = useState(false); 6 | const handleMouseEnter = () => { 7 | setIsHover(true); 8 | }; 9 | const handleMouseLeave = () => { 10 | setIsHover(false); 11 | }; 12 | 13 | //recording 14 | const recordButtonShape: React.CSSProperties = { 15 | display: 'flex', 16 | flexDirection: 'row', 17 | alignItems: 'center', 18 | position: 'absolute', 19 | width: '252px', 20 | height: '48px', 21 | // left: '1482px', 22 | // top: '1081px', 23 | borderRadius: '42px', 24 | justifyContent: 'center', 25 | padding: '14px 24px', 26 | columnGap: '16px', 27 | background: '#181818', 28 | border: '1px solid rgba(243, 246, 248, 0.1)', 29 | boxShadow: 30 | '0px 18px 24px rgba(0, 0, 0, 0.16), 0px 12px 16px rgba(6, 9, 11, 0.1), 0px 6px 12px rgba(0, 0, 0, 0.18), 0px 1px 20px rgba(0, 0, 0, 0.12)', 31 | cursor: 'pointer', 32 | bottom: '20px', 33 | }; 34 | 35 | const recordIcon: React.CSSProperties = { 36 | width: '20px', 37 | height: '20px', 38 | background: '#D75959', 39 | opacity: '0.8', 40 | boxShadow: 41 | '0px 2px 6px rgba(215, 89, 89, 0.24), 0px 6px 10px rgba(215, 89, 89, 0.2), 0px 1px 16px rgba(215, 89, 89, 0.06)', 42 | borderRadius: '30px', 43 | flex: 'none', 44 | order: '0', 45 | flexGrow: '0', 46 | animation: !isHover ? 'glowing 1500ms infinite' : 'none', 47 | }; 48 | 49 | const glowingAnimation = ` 50 | @keyframes glowing { 51 | 0% { background-color: #D75959; box-shadow: 0 0 3px #D75959; } 52 | 50% { background-color: #ce4949; box-shadow: 0 0 30px #D75959; } 53 | 100% { background-color: #D75959; box-shadow: 0 0 3px #D75959; } 54 | } 55 | 56 | .glowing { 57 | animation: glowing 1500ms infinite; 58 | } 59 | `; 60 | 61 | const recordText: React.CSSProperties = { 62 | fontSize: '14px', 63 | lineHeight: '16px', 64 | color: '#CB4F4F', 65 | opacity: '0.8', 66 | flex: 'none', 67 | order: '1', 68 | flexGrow: '0', 69 | }; 70 | 71 | //stop 72 | const stopButtonShape: React.CSSProperties = { 73 | display: 'flex', 74 | fontSize: '14px', 75 | flexDirection: 'row', 76 | alignItems: 'center', 77 | position: 'absolute', 78 | justifyContent: 'center', 79 | width: '252px', 80 | height: '48px', 81 | // left: '1482px', 82 | // top: '1081px', 83 | borderRadius: '42px', 84 | padding: '14px 24px', 85 | columnGap: '16px', 86 | background: '#212121', 87 | border: '1px solid rgba(243, 246, 248, 0.1)', 88 | boxShadow: 89 | '0px 18px 24px rgba(0, 0, 0, 0.16), 0px 12px 16px rgba(6, 9, 11, 0.1), 0px 6px 12px rgba(0, 0, 0, 0.18), 0px 1px 20px rgba(0, 0, 0, 0.12)', 90 | cursor: 'pointer', 91 | bottom: '20px', 92 | }; 93 | 94 | const stopIcon: React.CSSProperties = { 95 | width: '14px', 96 | height: '16px', 97 | background: 'rgba(243, 246, 248, 0.9)', 98 | // opacity: '0.8', 99 | // boxShadow: '0px 2px 6px rgba(215, 89, 89, 0.24), 0px 6px 10px rgba(215, 89, 89, 0.2), 0px 1px 16px rgba(215, 89, 89, 0.06)', 100 | borderRadius: '1px', 101 | flex: 'none', 102 | order: '0', 103 | flexGrow: '0', 104 | }; 105 | 106 | const stopText: React.CSSProperties = { 107 | height: '16px', 108 | fontSize: '14px', 109 | lineHeight: '16px', 110 | color: '#F3F6F8', 111 | opacity: '0.8', 112 | flex: 'none', 113 | order: '1', 114 | flexGrow: '0', 115 | }; 116 | 117 | return ( 118 | <> 119 | 120 | 132 | 133 | ); 134 | }; 135 | 136 | export default Record; 137 | -------------------------------------------------------------------------------- /package/zustand_generator/src/output/output-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Transaction, InitialRender } from '../types'; 3 | // } from '../types'; 4 | 5 | import { dummyParam } from '../utils/utils'; 6 | 7 | /* eslint-enable */ 8 | 9 | /* ----- HELPER FUNCTIONS ----- */ 10 | 11 | export function importZustandStore(): string { 12 | return `import useStore from '';`; 13 | } 14 | 15 | export function testInitialState(initialRender: InitialRender): string { 16 | return Object.entries(initialRender).reduce((acc, [k, v]) => { 17 | return ( 18 | acc 19 | + `\tit('${k} should initialize correctly', () => {\n\t\texpect(result.current.${k}).toStrictEqual(${JSON.stringify( 20 | v, 21 | )});\n\t});\n\n` 22 | ); 23 | }, ''); 24 | } 25 | 26 | const dummyTransaction = { action: dummyParam, changedValues: {} }; 27 | 28 | //Takes in an array of transactions and returns a full set of tests ("it blocks") for all actions and corresponding state changes 29 | export function testStateChangesAct(transactions: Transaction[]): string { 30 | //Groups transactions together based on whether the transactions impact the same slice of state 31 | //Each "group" of transactions will affect each store parameter 0 or 1 times. 32 | let groupedTransactions: Transaction[][] = [...transactions, dummyTransaction].reduce( 33 | ( 34 | acc: { 35 | groups: Transaction[][]; 36 | currentGroup: Transaction[]; 37 | changedValues: { [nameOfChangedValue: string]: any }; 38 | }, 39 | cur, 40 | ) => { 41 | if ( 42 | Object.keys(cur.changedValues).some((v) => acc.changedValues[v]) 43 | || cur.action === dummyParam 44 | ) { 45 | acc.groups.push(acc.currentGroup); 46 | acc.currentGroup = [cur]; 47 | acc.changedValues = Object.keys(cur.changedValues).reduce((acc, k) => { 48 | acc[k] = true; 49 | return acc; 50 | }, {}); 51 | } else { 52 | acc.currentGroup.push(cur); 53 | Object.keys(cur.changedValues).forEach((k) => (acc.changedValues[k] = true)); 54 | } 55 | return acc; 56 | }, 57 | { groups: [], currentGroup: [], changedValues: {} }, 58 | ).groups; 59 | 60 | //For each group of transactions, we generate an "It block" 61 | return groupedTransactions.reduce( 62 | (acc, group) => { 63 | const { str, actBlock } = generateItBlock(group); 64 | acc.str += str; 65 | acc.actStatements = actBlock; 66 | return acc; 67 | }, 68 | { str: '', actStatements: '' }, 69 | ).str; 70 | } 71 | //Takes in an entry for a slice of state and generates an expect statement asserting that the state properties have correct value in the store 72 | export function testStateChangesExpect([propertyName, newValue]: [string, any]): string { 73 | return `\nexpect(result.current.${propertyName}).toStrictEqual(${JSON.stringify(newValue)});`; 74 | } 75 | 76 | //Takes in a transaction and generates an act statement using the action name and argument(s) 77 | export function generateActLine(t: Transaction): string { 78 | const { action } = t; 79 | const args = t.arguments; 80 | return `\tresult.current.${action}(${args?.map((arg) => JSON.stringify(arg)).join(', ')});\n`; 81 | } 82 | 83 | //Takes in an array of grouped Transactions and returns an It Block (unit test) with all act & 84 | // expect statements for transactions in input 85 | function generateItBlock(transactions: Transaction[]): { str: string; actBlock: string } { 86 | const valuesChanged: string[] = []; 87 | let expectBlock = ''; 88 | 89 | transactions.forEach((t) => 90 | Object.entries(t.changedValues).forEach(([changedValue, newValue]) => { 91 | valuesChanged.push(changedValue); 92 | expectBlock += testStateChangesExpect([changedValue, newValue]); 93 | }), 94 | ); 95 | 96 | let newActBlock = transactions.map(generateActLine).join(''); 97 | 98 | return { 99 | str: `\n\tit('${valuesChanged.join(' & ')} should update correctly', () => { 100 | const { result } = renderHook(useStore); 101 | 102 | act(() => {\n${newActBlock}\n}); 103 | 104 | ${expectBlock} 105 | });`, 106 | actBlock: newActBlock, 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /package/zustand_generator/__tests__/output-utils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | testInitialState, 3 | testStateChangesExpect, 4 | testStateChangesAct, 5 | generateActLine 6 | } from '../src/output/output-utils'; 7 | 8 | const initialRender = { 9 | todoListState: [], 10 | todoListFilterState: 'Show All', 11 | todoListSortState: false, 12 | quoteText: '', 13 | quoteNumber: 0, 14 | checkBox: false, 15 | } 16 | 17 | describe('INITIAL RENDER', () => { 18 | //create a variable to hold our expected output 19 | const expectedOutput = '' 20 | + `\tit('todoListState should initialize correctly', () => {\n\t\texpect(result.current.todoListState).toStrictEqual([]);\n\t});\n\n` 21 | + `\tit('todoListFilterState should initialize correctly', () => {\n\t\texpect(result.current.todoListFilterState).toStrictEqual("Show All");\n\t});\n\n` 22 | + `\tit('todoListSortState should initialize correctly', () => {\n\t\texpect(result.current.todoListSortState).toStrictEqual(false);\n\t});\n\n` 23 | + `\tit('quoteText should initialize correctly', () => {\n\t\texpect(result.current.quoteText).toStrictEqual("");\n\t});\n\n` 24 | + `\tit('quoteNumber should initialize correctly', () => {\n\t\texpect(result.current.quoteNumber).toStrictEqual(0);\n\t});\n\n` 25 | + `\tit('checkBox should initialize correctly', () => {\n\t\texpect(result.current.checkBox).toStrictEqual(false);\n\t});\n\n` 26 | //create a variable and assign it to the evaluated result of the calling the testInitialState on our input 27 | const evaluatedResult = testInitialState(initialRender); 28 | //expect(realOutput).toStrictEqual(expectedOutput); 29 | it('expectedOutput should equal evaulatedResult', () => { 30 | expect(evaluatedResult).toStrictEqual(expectedOutput); 31 | }); 32 | 33 | 34 | 35 | }) 36 | 37 | describe('TEST STATE CHANGES ACT', () => { 38 | 39 | const transaction = [ 40 | { 41 | action: 'setFilter', 42 | arguments: ['Show Uncompleted'], 43 | changedValues: { 'todoListFilterState': 'Show Uncompleted' } 44 | } 45 | ]; 46 | const testStateChangesActOutput = testStateChangesAct(transaction); 47 | 48 | const expectedOutput = 49 | 50 | `\n\tit('todoListFilterState should update correctly', () => { 51 | const { result } = renderHook(useStore); 52 | 53 | act(() => {\n result.current.setFilter("Show Uncompleted");\n}); 54 | 55 | \nexpect(result.current.todoListFilterState).toStrictEqual("Show Uncompleted"); 56 | });`; 57 | 58 | let expectedNoWhitespace = expectedOutput.replace(/\s/g, ''); 59 | 60 | 61 | it('expect output to equal expected output', () => { 62 | expect(testStateChangesActOutput.replace(/\s/g, '')).toStrictEqual(expectedNoWhitespace) 63 | }) 64 | }) 65 | 66 | 67 | 68 | describe('TEST STATE CHANGES EXPECT', () => { 69 | //1. Create function inputs manually 70 | const input = ["todoListFilterState", "Show Completed"]; 71 | //2. Manually write out expected output of function 72 | const expectedOutput = `\nexpect(result.current.todoListFilterState).toStrictEqual("Show Completed");` 73 | //3. Run function to get actual output 74 | //4. Compare exptected ouptut to actual output 75 | const testStateChanges = testStateChangesExpect(input) 76 | 77 | it('it should be true when when input is passed into testStateChanges function ', () => { 78 | expect(testStateChanges).toStrictEqual(expectedOutput); 79 | }) 80 | }) 81 | 82 | 83 | 84 | 85 | describe('GENERATE ACT LINE', () => { 86 | // Create a tranaction inputs manually 87 | const transaction = { 88 | action: 'setFilter', 89 | arguments: ['Show Completed'], 90 | changedValues: { todoListFilterState: "Show Completed" } 91 | }; 92 | const action = transaction.action; 93 | const args = transaction.arguments; 94 | 95 | const expectedOutput = `\tresult.current.${action}(${args?.map(arg => JSON.stringify(arg)).join(', ')});\n` 96 | 97 | const testActGeneration = generateActLine(transaction) 98 | // Manually writing out expected output of function 99 | //const evaluateActGeneration = `\tresult.current.${action}(${args?.map(arg => JSON.stringify(arg)).join(', ')});\n` 100 | // Run func to get actual ouput 101 | //Compare step2 to step 3 102 | it('testActGeneration should generate an action line', () => { 103 | expect(expectedOutput).toStrictEqual(testActGeneration) 104 | }) 105 | }) -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen", 3 | "version": "5.0.1", 4 | "description": "simple, interaction-driven Jest test generator for Recoil and React Hooks 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 | "localUpdate": "tsc && npm --prefix ../demo-zustand-todo run update", 20 | "tarballUpdate": "npm --prefix ../demo-zustand-todo run symlink", 21 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/open-source-labs/Chromogen.git" 26 | }, 27 | "contributors": [ 28 | { 29 | "name": "Brach Burdick", 30 | "url": "https://github.com/sirbrachthepale/" 31 | }, 32 | { 33 | "name": "Francois Denavaut", 34 | "url": "https://github.com/dnvt/" 35 | }, 36 | { 37 | "name": "Maggie Kwan", 38 | "url": "https://github.com/maggiekwan/" 39 | }, 40 | { 41 | "name": "Lawrence Liang", 42 | "url": "https://github.com/Lawliang/" 43 | }, 44 | { 45 | "name": "Michelle Holland", 46 | "url": "https://github.com/michellebholland/" 47 | }, 48 | { 49 | "name": "Jim Chen", 50 | "url": "https://github.com/chenchingk" 51 | }, 52 | { 53 | "name": "Andy Wang", 54 | "url": "https://github.com/andywang23" 55 | }, 56 | { 57 | "name": "Connor Rose Delisle", 58 | "url": "https://github.com/connorrose" 59 | }, 60 | { 61 | "name": "Amy Yee", 62 | "url": "https://github.com/amyy98" 63 | }, 64 | { 65 | "name": "Cameron Greer", 66 | "url": "https://github.com/cgreer011" 67 | }, 68 | { 69 | "name": "Jinseon Shin", 70 | "url": "https://github.com/wlstjs" 71 | }, 72 | { 73 | "name": "Nicholas Shay", 74 | "url": "https://github.com/nicholasjs" 75 | }, 76 | { 77 | "name": "Ryan Tumel", 78 | "url": "https://github.com/rtumel123" 79 | }, 80 | { 81 | "name": "Marcellies Pettiford", 82 | "url": "https://github.com/mp-04" 83 | }, 84 | { 85 | "name": "Sung Kim", 86 | "url": "https://github.com/smk53664" 87 | }, 88 | { 89 | "name": "Lina Lee", 90 | "url": "https://github.com/lina4lee" 91 | }, 92 | { 93 | "name": "Erica Oh", 94 | "url": "https://github.com/ericaysoh" 95 | }, 96 | { 97 | "name": "Dani Almaraz", 98 | "url": "https://github.com/dtalmaraz" 99 | }, 100 | { 101 | "name": "Craig Boswell", 102 | "url": "https://github.com/crgb0s" 103 | }, 104 | { 105 | "name": "Hussein Ahmed", 106 | "url": "https://github.com/Hali3030" 107 | }, 108 | { 109 | "name": "Ian Kila", 110 | "url": "https://github.com/iannkila" 111 | }, 112 | { 113 | "name": "Yuehao Wong", 114 | "url": "https://github.com/yuehaowong" 115 | } 116 | ], 117 | "license": "MIT", 118 | "bugs": { 119 | "url": "https://github.com/open-source-labs/Chromogen/issues" 120 | }, 121 | "homepage": "https://github.com/open-source-labs/Chromogen#readme", 122 | "peerDependencies": { 123 | "jest": ">=24.0.0", 124 | "typescript": ">=3.8.0" 125 | }, 126 | "dependencies": { 127 | "@uiw/react-textarea-code-editor": "^2.1.1", 128 | "dependency-cruiser": "^12.9.0", 129 | "react": "^18.0.0", 130 | "react-dom": "^18.0.0", 131 | "recoil": "^0.7.2", 132 | "redux": "^4.0.5", 133 | "styled-components": "^5.3.6", 134 | "zustand": "^4.1.1" 135 | }, 136 | "devDependencies": { 137 | "@babel/core": "^7.11.6", 138 | "@babel/preset-env": "^7.11.5", 139 | "@babel/preset-react": "^7.10.4", 140 | "@babel/preset-typescript": "^7.10.4", 141 | "@testing-library/react": "^13.1.1", 142 | "@types/node": "^14.11.2", 143 | "@types/react": "^18.0.6", 144 | "@types/react-dom": "^18.0.2", 145 | "@types/styled-components": "^5.1.26", 146 | "babel-jest": "^26.3.0", 147 | "coveralls": "^3.1.0", 148 | "css-loader": "^6.7.3", 149 | "eslint-config-airbnb-typescript": "^17.0.0", 150 | "jest": "^26.6.3", 151 | "react-test-renderer": "^18.0.0", 152 | "typescript": "^4.0.3" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/store/store.js: -------------------------------------------------------------------------------- 1 | import { chromogenZustandMiddleware } from 'chromogen'; 2 | import { create } from 'zustand'; 3 | 4 | const useToDoStore = create( 5 | chromogenZustandMiddleware((set) => ({ 6 | todoListState: [], 7 | 8 | todoListFilterState: 'Show All', 9 | 10 | todoListSortState: false, 11 | 12 | resetFiltersAndSorted: () => 13 | set( 14 | () => ({ todoListFilterState: 'Show All', todoListSortState: false }), 15 | false, 16 | 'resetFiltersAndSorted', 17 | ), 18 | 19 | toggleSort: () => 20 | set((state) => ({ todoListSortState: !state.todoListSortState }), false, 'toggleSort'), 21 | 22 | setFilter: (filter) => set(() => ({ todoListFilterState: filter }), false, 'setFilter'), 23 | 24 | quoteText: '', 25 | 26 | changeQuoteText: (text) => set(() => ({ quoteText: text }), false, 'changeQuoteText', text), 27 | 28 | quoteNumber: 0, 29 | 30 | changeQuoteNumber: () => 31 | set(() => ({ quoteNumber: Math.floor(Math.random() * 1643) }), false, 'changeQuoteNumber'), 32 | 33 | setAllComplete: () => 34 | set( 35 | (state) => ({ 36 | todoListState: state.todoListState.some((todo) => todo.isComplete === false) 37 | ? state.todoListState.map((todo) => { 38 | return { ...todo, isComplete: true }; 39 | }) 40 | : state.todoListState.map((todo) => { 41 | return { ...todo, isComplete: false }; 42 | }), 43 | }), 44 | false, 45 | 'setAllComplete', 46 | ), 47 | 48 | checkBox: false, 49 | 50 | setCheckBox: () => 51 | set( 52 | (state) => ({ 53 | checkBox: state.todoListState.some((todo) => todo.isComplete === false) ? false : true, 54 | }), 55 | false, 56 | 'setCheckBox', 57 | ), 58 | 59 | addTodoListItem: (todo) => 60 | set( 61 | (state) => ({ todoListState: [...state.todoListState, todo] }), 62 | false, 63 | 'addTodoListItem', 64 | todo, 65 | ), 66 | 67 | deleteTodoListItem: (id) => 68 | set( 69 | (state) => ({ todoListState: state.todoListState.filter((todo) => todo.id !== id) }), 70 | false, 71 | 'deleteTodoListItem', 72 | id, 73 | ), 74 | 75 | editItemText: (text, id) => 76 | set( 77 | (state) => ({ 78 | todoListState: state.todoListState.map((todo) => { 79 | if (todo.id === id) { 80 | return { ...todo, text: text }; 81 | } else { 82 | return todo; 83 | } 84 | }), 85 | }), 86 | false, 87 | 'editItemText', 88 | text, 89 | id, 90 | ), 91 | 92 | toggleItemCompletion: (id) => 93 | set( 94 | (state) => ({ 95 | todoListState: state.todoListState.map((todo) => { 96 | if (todo.id === id) { 97 | return { ...todo, isComplete: !todo.isComplete }; 98 | } else { 99 | return todo; 100 | } 101 | }), 102 | }), 103 | false, 104 | 'toggleItemCompletion', 105 | id, 106 | ), 107 | 108 | searchResultState: { 109 | all: { 110 | searchTerm: '', 111 | results: [], 112 | }, 113 | high: { 114 | searchTerm: '', 115 | results: [], 116 | }, 117 | medium: { 118 | searchTerm: '', 119 | results: [], 120 | }, 121 | low: { 122 | searchTerm: '', 123 | results: [], 124 | }, 125 | }, 126 | 127 | setSearchState: (searchTerm, priority) => 128 | set( 129 | (state) => { 130 | if (searchTerm === '') 131 | return { 132 | searchResultState: { 133 | ...state.searchResultState, 134 | [priority]: { searchTerm, results: [] }, 135 | }, 136 | }; 137 | let results = [...state.todoListState].filter((todo) => todo.text.includes(searchTerm)); 138 | if (priority !== 'all') results = results.filter((todo) => todo.priority === priority); 139 | return { 140 | searchResultState: { ...state.searchResultState, [priority]: { searchTerm, results } }, 141 | }; 142 | }, 143 | false, 144 | 'setSearchState', 145 | searchTerm, 146 | priority, 147 | ), 148 | })), 149 | ); 150 | 151 | export default useToDoStore; 152 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/panel.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React, { useState, useEffect } from 'react'; 3 | import { useStore } from '../utils/store'; 4 | import { styles, generateFile, generateTests } from './component-utils'; 5 | /* eslint-enable */ 6 | 7 | /* using a zustand store to keep track of recording state */ 8 | const selector = (state) => ({ 9 | recording: state.recording, 10 | toggleRecording: state.toggleRecording, 11 | }); 12 | 13 | export const Panel: React.FC = () => { 14 | // Initializing as undefined over null to match React typing for AnchorHTML attributes 15 | const [file, setFile] = useState(undefined); 16 | const [storeMap] = useState>(new Map()); 17 | const { recording, toggleRecording } = useStore<{ 18 | recording: boolean; 19 | toggleRecording: Function; 20 | }>(selector); 21 | 22 | // Auto-click download link when a new file is generated (via button click) 23 | useEffect(() => document.getElementById('chromogen-download')!.click(), [file]); 24 | // ! to get around strict null check in tsconfig 25 | 26 | const [pauseColor, setPauseColor] = useState('#90d1f0'); 27 | const pauseBorderStyle = { 28 | borderColor: `${pauseColor}`, 29 | }; 30 | 31 | const [playColor, setPlayColor] = useState('transparent transparent transparent #90d1f0'); 32 | const playBorderStyle = { 33 | borderColor: `${playColor}`, 34 | }; 35 | 36 | return ( 37 | <> 38 | { 39 | 115 | } 116 | 122 | Download Test 123 | 124 | 125 | ); 126 | }; 127 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import SecondaryButton from './Buttons/SecondaryButton'; 3 | import { generateFile, generateTests } from './component-utils'; 4 | 5 | const toolBar: React.CSSProperties = { 6 | display: 'flex', 7 | height: '56px', 8 | width: '100%', 9 | padding: '8px 16px', 10 | alignItems: 'center', 11 | gap: '8px', 12 | borderBottom: `1px solid rgba(243,246,248,.05)`, 13 | }; 14 | 15 | const toolBarLogoBox: React.CSSProperties = { 16 | height: '40px', 17 | width: '40px', 18 | display: 'flex', 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | }; 22 | 23 | const toolBarTitleContainer: React.CSSProperties = { 24 | display: 'flex', 25 | flexDirection: 'column', 26 | color: '#fff', 27 | lineHeight: '16px', 28 | opacity: 0.9, 29 | marginLeft: '8px', 30 | rowGap: '4px', 31 | flexGrow: 1, 32 | }; 33 | 34 | const toolBarTitle: React.CSSProperties = { 35 | fontSize: '16px', 36 | fontWeight: 500, 37 | fontFamily: 'Inter !important', 38 | }; 39 | 40 | const toolBarDescription: React.CSSProperties = { 41 | fontSize: '12px', 42 | fontWeight: 400, 43 | }; 44 | 45 | export const copyButton: React.CSSProperties = { 46 | height: '32px', 47 | width: '32px', 48 | backgroundColor: 'rgb(243, 246, 248, 0.03)', 49 | border: '1px solid rgba(243, 246, 248, 0.05)', 50 | borderRadius: '6px', 51 | display: 'flex', 52 | alignItems: 'center', 53 | padding: '6px', 54 | }; 55 | 56 | export const minimizeButton: React.CSSProperties = { 57 | height: '24px', 58 | width: '24px', 59 | backgroundColor: 'white', 60 | marginLeft: '10px', 61 | padding: 0, 62 | cursor: 'pointer', 63 | }; 64 | 65 | export const minimizeIcon: React.CSSProperties = {}; 66 | 67 | export const Header = ({ isHidden, setIsHidden }) => { 68 | const [file, setFile] = useState(undefined); 69 | const [copied, setCopied] = useState(false); 70 | 71 | useEffect(() => { 72 | copyOff(); 73 | }, [copied]); 74 | 75 | function copyOff() { 76 | setTimeout(() => { 77 | setCopied(false); 78 | }, 3000); 79 | } 80 | 81 | return ( 82 |
83 |
84 |
85 | 92 | 93 | 94 | 95 | 96 | 101 | 102 | 103 |
104 |
105 |
106 |
Chromogen Tests
107 |
Interact with the app to generate tests
108 |
109 | 115 | generateFile(setFile, new Map())} 120 | /> 121 | 122 | { 125 | navigator.clipboard.writeText(generateTests(new Map())[0]); 126 | setCopied(true); 127 | }} 128 | /> 129 | setIsHidden(!isHidden)} /> 130 |
131 | ); 132 | }; 133 | -------------------------------------------------------------------------------- /demo-todo/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* -------------General Styles---------------- */ 2 | html { 3 | margin: 0; 4 | background-color: rgb(48, 48, 48); 5 | color: whitesmoke; 6 | font-family: 'Palanquin', sans-serif; 7 | overflow: hidden; 8 | } 9 | 10 | h1 { 11 | text-align: center; 12 | font-size: 2.3rem; 13 | color: #af6358; 14 | text-shadow: 1px 2px 2px rgba(250, 250, 250, 0.267); 15 | letter-spacing: 3px; 16 | font-style: italic; 17 | } 18 | input, 19 | button, 20 | select { 21 | background-color: rgb(63, 63, 63); 22 | border: 1px solid lightgray; 23 | border: none; 24 | padding: 20px; 25 | border-radius: 4px; 26 | color: whitesmoke; 27 | font-size: 16px; 28 | letter-spacing: 1px; 29 | } 30 | 31 | /* remove browser defaults */ 32 | button:focus { 33 | outline: none; 34 | } 35 | input:focus { 36 | outline: none; 37 | } 38 | /* ------------TodoList------------- */ 39 | 40 | /* topmost container */ 41 | .mainContainer { 42 | display: grid; 43 | height: 100vh; 44 | width: 100vw; 45 | grid-template-rows: 15fr 33fr 33fr; 46 | } 47 | 48 | /* overall row container for todo list display */ 49 | .todosDisplayRow { 50 | margin: 0 auto; 51 | width: 700px; 52 | } 53 | 54 | /* container for entire list display */ 55 | .todosContainer { 56 | background-color: rgb(63, 63, 63); 57 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 58 | border-radius: 5px; 59 | padding: 5px 12px 0 12px; 60 | } 61 | 62 | /* -------------TodoItemCreator---------------- */ 63 | 64 | .itemCreator button { 65 | padding: 0px; 66 | } 67 | 68 | input::placeholder { 69 | font-style: italic; 70 | letter-spacing: 1.5px; 71 | } 72 | 73 | .itemCreator input { 74 | padding: 0 0 0 5%; 75 | width: 70%; 76 | height: 60px; 77 | } 78 | 79 | #radioContainer svg { 80 | margin-top: 11px; 81 | opacity: 0.7; 82 | } 83 | 84 | /* -------------TodoItem---------------- */ 85 | 86 | .itemContainer, 87 | .lowPriority, 88 | .mediumPriority, 89 | .highPriority { 90 | display: grid; 91 | grid-template-columns: 79fr 14fr 7fr; 92 | border-bottom: 1px solid rgba(245, 245, 245, 0.336); 93 | } 94 | 95 | /* dynamic checkbox color */ 96 | .highPriority svg { 97 | color: #ef5350; 98 | opacity: 0.7; 99 | margin-right: 28px; 100 | } 101 | .mediumPriority svg { 102 | color: #ffee58; 103 | opacity: 0.7; 104 | margin-right: 28px; 105 | } 106 | .lowPriority svg { 107 | color: #66bb6a; 108 | opacity: 0.7; 109 | margin-right: 28px; 110 | } 111 | 112 | #todoItem button { 113 | margin-left: 7px; 114 | } 115 | 116 | /* -------------TodoListFilter---------------- */ 117 | 118 | ul { 119 | display: grid; 120 | grid-template-columns: 27fr 27fr 27fr 6fr 6fr 6fr; 121 | margin: 0; 122 | padding: 0px; 123 | width: 100%; 124 | } 125 | 126 | .filter-button { 127 | margin-top: 10px; 128 | margin-bottom: 10px; 129 | border-right: 1px solid rgba(143, 143, 143, 0.26); 130 | padding: 10px 20px; 131 | border-radius: 0px 0px 4px 4px; 132 | } 133 | 134 | /* dynamic sort icon color */ 135 | #sortedWhite svg { 136 | color: whitesmoke; 137 | } 138 | #unsortedGray svg { 139 | color: rgba(245, 245, 245, 0.336); 140 | } 141 | #unsortedGray { 142 | width: 64px; 143 | } 144 | 145 | #statsSpan { 146 | display: grid; 147 | grid-template-columns: 30fr 30fr 30fr; 148 | align-items: center; 149 | } 150 | #highSpan { 151 | color: #ef5350; 152 | opacity: 0.7; 153 | margin-right: 4px; 154 | } 155 | #mediumSpan { 156 | color: #ffee58; 157 | opacity: 0.7; 158 | margin-right: 4px; 159 | } 160 | #lowSpan { 161 | color: #66bb6a; 162 | opacity: 0.7; 163 | } 164 | 165 | /* filter stats (number) */ 166 | button span { 167 | opacity: 0.6; 168 | } 169 | 170 | /* ----------QuoteBox---------- */ 171 | 172 | #quoteContainer { 173 | display: flex; 174 | flex-direction: column; 175 | justify-content: space-between; 176 | } 177 | 178 | #quoteContainer p { 179 | white-space: pre-wrap; 180 | } 181 | 182 | .quoteBox { 183 | display: flex; 184 | flex-direction: row; 185 | justify-content: space-between; 186 | background-color: rgb(63, 63, 63); 187 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 188 | border-radius: 5px; 189 | margin: auto; 190 | padding: 12px; 191 | } 192 | 193 | .quoteBox img { 194 | margin: 20px; 195 | height: 150px; 196 | width: 150px; 197 | } 198 | 199 | .quoteBox button { 200 | width: 150px; 201 | } 202 | 203 | .quoteBox button:hover { 204 | border: 1px solid whitesmoke; 205 | cursor: pointer; 206 | } 207 | 208 | /* ---------- TodoQuickCheck ---------- */ 209 | 210 | #quickCheck { 211 | font-family: Arial, Helvetica, sans-serif; 212 | } 213 | 214 | #quickCheck svg { 215 | color: #af6358; 216 | } 217 | 218 | /* ---------- Search ---------- */ 219 | .searchContainer { 220 | margin-top: 100px; 221 | background-color: rgb(63, 63, 63); 222 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 223 | border-radius: 5px; 224 | padding: 5px 12px 12px 12px; 225 | display: grid; 226 | grid-template-columns: 70% 30%; 227 | } 228 | 229 | .searchField { 230 | border-bottom: solid 1px whitesmoke; 231 | border-radius: 0; 232 | } 233 | 234 | .prioritySelect { 235 | grid-column-start: 2; 236 | border-bottom: solid 1px whitesmoke; 237 | border-radius: 0; 238 | } 239 | 240 | .searchResults { 241 | grid-column-start: span 3; 242 | } 243 | -------------------------------------------------------------------------------- /package/recoil_generator/src/component/component-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { CSSProperties } from 'react'; 3 | import type { SerializableParam } from 'recoil'; 4 | import type { Ledger } from '../types'; 5 | 6 | import { ledger } from '../utils/ledger'; 7 | import { convertFamilyTrackerKeys } from '../utils/utils'; 8 | import { output } from '../output/output'; 9 | /* eslint-enable */ 10 | 11 | const buttonStyle: CSSProperties = { 12 | display: 'inline-block', 13 | margin: '8px', 14 | marginLeft: '13px', 15 | padding: '0px', 16 | height: '25px', 17 | width: '65px', 18 | borderRadius: '4px', 19 | justifyContent: 'space-evenly', 20 | border: '1px', 21 | cursor: 'pointer', 22 | color: '#90d1f0', 23 | fontSize: '10px', 24 | }; 25 | 26 | const divStyle: CSSProperties = { 27 | display: 'flex', 28 | position: 'absolute', 29 | bottom: '100px', 30 | left: '100px', 31 | backgroundColor: '#aaa', 32 | borderRadius: '4px', 33 | margin: 0, 34 | padding: 0, 35 | zIndex: 999999, 36 | }; 37 | 38 | const playStyle: CSSProperties = { 39 | boxSizing: 'border-box', 40 | marginLeft: '25px', 41 | borderStyle: 'solid', 42 | borderWidth: '7px 0px 7px 14px', 43 | }; 44 | 45 | const pauseStyle: CSSProperties = { 46 | width: '14px', 47 | height: '14px', 48 | borderWidth: '0px 0px 0px 10px', 49 | borderStyle: 'double', 50 | marginLeft: '27px', 51 | }; 52 | 53 | export const styles = { buttonStyle, divStyle, playStyle, pauseStyle }; 54 | 55 | /** 56 | * onclick function that generates test file & sets download URL 57 | * 58 | * Key-to-Variable name mapping is applied if storeMap has any contents 59 | * (meaning atom / selector nodes were passed as props) 60 | * Applying only at point-of-download keeps performance cost low for users who 61 | * don't need to pass nodes while creating a moderate performance hit for others 62 | * only while downloading, never while interacting with their app. 63 | */ 64 | export const generateTests = (storeMap: Map): string[] => { 65 | const { 66 | atoms, 67 | selectors, 68 | setters, 69 | atomFamilies, 70 | selectorFamilies, 71 | initialRender, 72 | initialRenderFamilies, 73 | transactions, 74 | setTransactions, 75 | } = ledger; 76 | 77 | const finalLedger: Ledger = 78 | storeMap.size > 0 79 | ? { 80 | atoms: atoms.map(({ key }) => storeMap.get(key) || key), 81 | selectors: selectors.map((key) => storeMap.get(key) || key), 82 | atomFamilies: convertFamilyTrackerKeys(atomFamilies, storeMap), 83 | selectorFamilies: convertFamilyTrackerKeys(selectorFamilies, storeMap), 84 | setters: setters.map((key) => storeMap.get(key) || key), 85 | initialRender: initialRender.map(({ key, value }) => { 86 | const newKey = storeMap.get(key) || key; 87 | return { key: newKey, value }; 88 | }), 89 | initialRenderFamilies: initialRenderFamilies.map(({ key, value, params }) => { 90 | const newKey = storeMap.get(key) || key; 91 | return { key: newKey, value, params }; 92 | }), 93 | transactions: transactions.map(({ state, updates, atomFamilyState, familyUpdates }) => { 94 | const newState = state.map((eachAtom) => { 95 | const key = storeMap.get(eachAtom.key) || eachAtom.key; 96 | return { ...eachAtom, key }; 97 | }); 98 | const newUpdates = updates.map((eachSelector) => { 99 | const key = storeMap.get(eachSelector.key) || eachSelector.key; 100 | const { value } = eachSelector; 101 | return { key, value }; 102 | }); 103 | const newAtomFamilyState = atomFamilyState.map((eachFamAtom) => { 104 | const family = storeMap.get(eachFamAtom.family) || eachFamAtom.family; 105 | const oldKey = eachFamAtom.key; 106 | const keySuffix = oldKey.substring(eachFamAtom.family.length); 107 | const key = family + keySuffix; 108 | return { ...eachFamAtom, family, key }; 109 | }); 110 | const newFamilyUpdates = familyUpdates.map((eachFamSelector) => { 111 | const key = storeMap.get(eachFamSelector.key) || eachFamSelector.key; 112 | return { ...eachFamSelector, key }; 113 | }); 114 | return { 115 | state: newState, 116 | updates: newUpdates, 117 | atomFamilyState: newAtomFamilyState, 118 | familyUpdates: newFamilyUpdates, 119 | }; 120 | }), 121 | setTransactions: setTransactions.map(({ state, setter }) => { 122 | const newState = state.map((eachAtom) => { 123 | const key = storeMap.get(eachAtom.key) || eachAtom.key; 124 | return { ...eachAtom, key }; 125 | }); 126 | const newSetter = setter; 127 | if (newSetter) { 128 | const { key } = newSetter; 129 | newSetter.key = storeMap.get(key) || key; 130 | } 131 | return { state: newState, setter: newSetter }; 132 | }), 133 | } 134 | : { ...ledger, atoms: atoms.map(({ key }) => key) }; 135 | 136 | //return setFile(URL.createObjectURL(new Blob([output(finalLedger)]))); 137 | return [output(finalLedger)]; 138 | }; 139 | 140 | export const generateFile = (setFile: Function, storeMap: Map): string[] => { 141 | const tests = generateTests(storeMap); 142 | const blob = new Blob(tests); 143 | setFile(URL.createObjectURL(blob)); 144 | return tests; 145 | }; 146 | -------------------------------------------------------------------------------- /demo-todo/__tests__/initialTestTest.js: -------------------------------------------------------------------------------- 1 | import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library'; 2 | import { useRecoilValue, useRecoilState } from 'recoil'; 3 | import { 4 | filteredTodoListState, 5 | sortedTodoListState, 6 | todoListSortedStats, 7 | todoListStatsState, 8 | filteredListContentState, 9 | allCompleteState, 10 | refreshFilterState, 11 | searchBarSelectorFam, 12 | 13 | } from '../src/store/store'; 14 | import { 15 | todoListState, 16 | todoListFilterState, 17 | todoListSortState, 18 | quoteNumberState, 19 | searchResultState, 20 | 21 | } from '../src/store/atoms'; 22 | 23 | // Suppress 'Batcher' warnings from React / Recoil conflict 24 | console.error = jest.fn(); 25 | 26 | // Hook to return atom/selector values and/or modifiers for react-recoil-hooks-testing-library 27 | const useStoreHook = () => { 28 | // atoms 29 | const [todoListStateValue, settodoListState] = useRecoilState(todoListState); 30 | const [todoListFilterStateValue, settodoListFilterState] = useRecoilState(todoListFilterState); 31 | const [todoListSortStateValue, settodoListSortState] = useRecoilState(todoListSortState); 32 | const [quoteNumberStateValue, setquoteNumberState] = useRecoilState(quoteNumberState); 33 | const [searchResultStateValue, setsearchResultState] = useRecoilState(searchResultState); 34 | 35 | // writeable selectors 36 | const [allCompleteStateValue, setallCompleteState] = useRecoilState(allCompleteState); 37 | const [refreshFilterStateValue, setrefreshFilterState] = useRecoilState(refreshFilterState); 38 | 39 | // read-only selectors 40 | const filteredTodoListStateValue = useRecoilValue(filteredTodoListState); 41 | const sortedTodoListStateValue = useRecoilValue(sortedTodoListState); 42 | const todoListSortedStatsValue = useRecoilValue(todoListSortedStats); 43 | const todoListStatsStateValue = useRecoilValue(todoListStatsState); 44 | const filteredListContentStateValue = useRecoilValue(filteredListContentState); 45 | 46 | // atom families 47 | 48 | // writeable selector families 49 | 50 | // read-only selector families 51 | 52 | 53 | 54 | 55 | return { 56 | todoListStateValue, 57 | settodoListState, 58 | todoListFilterStateValue, 59 | settodoListFilterState, 60 | todoListSortStateValue, 61 | settodoListSortState, 62 | quoteNumberStateValue, 63 | setquoteNumberState, 64 | searchResultStateValue, 65 | setsearchResultState, 66 | allCompleteStateValue, 67 | setallCompleteState, 68 | refreshFilterStateValue, 69 | setrefreshFilterState, 70 | filteredTodoListStateValue, 71 | sortedTodoListStateValue, 72 | todoListSortedStatsValue, 73 | todoListStatsStateValue, 74 | filteredListContentStateValue, 75 | }; 76 | }; 77 | 78 | describe('INITIAL RENDER', () => { 79 | const { result } = renderRecoilHook(useStoreHook); 80 | 81 | it('filteredTodoListState should initialize correctly', () => { 82 | expect(result.current.filteredTodoListStateValue).toStrictEqual([]); 83 | }); 84 | 85 | it('sortedTodoListState should initialize correctly', () => { 86 | expect(result.current.sortedTodoListStateValue).toStrictEqual([]); 87 | }); 88 | 89 | it('allCompleteState should initialize correctly', () => { 90 | expect(result.current.allCompleteStateValue).toStrictEqual(true); 91 | }); 92 | 93 | it('filteredListContentState should initialize correctly', () => { 94 | expect(result.current.filteredListContentStateValue).toStrictEqual(false); 95 | }); 96 | 97 | it('todoListSortedStats should initialize correctly', () => { 98 | expect(result.current.todoListSortedStatsValue).toStrictEqual({}); 99 | }); 100 | 101 | it('todoListStatsState should initialize correctly', () => { 102 | expect(result.current.todoListStatsStateValue).toStrictEqual({ "totalNum": 0, "totalCompletedNum": 0, "totalUncompletedNum": 0, "percentCompleted": 0 }); 103 | }); 104 | 105 | 106 | }); 107 | 108 | describe('SELECTORS', () => { 109 | it('todoListSortedStats should properly derive state when todoListState updates', () => { 110 | const { result } = renderRecoilHook(useStoreHook); 111 | 112 | act(() => { 113 | result.current.settodoListState([{ "id": 1, "text": "tennis", "priority": "low", "isComplete": false }]); 114 | 115 | result.current.settodoListFilterState("Show All"); 116 | 117 | result.current.settodoListSortState(false); 118 | 119 | result.current.setquoteNumberState(23); 120 | 121 | result.current.setsearchResultState({ "all": { "searchTerm": "", "results": [] }, "high": { "searchTerm": "", "results": [] }, "medium": { "searchTerm": "", "results": [] }, "low": { "searchTerm": "", "results": [] } }); 122 | 123 | 124 | 125 | }); 126 | expect(result.current.todoListSortedStatsValue).toStrictEqual({ "low": 1 }); 127 | 128 | }); 129 | 130 | it('todoListSortedStats should properly derive state when todoListState updates', () => { 131 | const { result } = renderRecoilHook(useStoreHook); 132 | 133 | act(() => { 134 | result.current.settodoListState([{ "id": 1, "text": "tennis", "priority": "low", "isComplete": false }, { "id": 2, "text": "chinese chicken", "priority": "low", "isComplete": false }]); 135 | 136 | result.current.settodoListFilterState("Show All"); 137 | 138 | result.current.settodoListSortState(false); 139 | 140 | result.current.setquoteNumberState(23); 141 | 142 | result.current.setsearchResultState({ "all": { "searchTerm": "", "results": [] }, "high": { "searchTerm": "", "results": [] }, "medium": { "searchTerm": "", "results": [] }, "low": { "searchTerm": "", "results": [] } }); 143 | 144 | 145 | 146 | }); 147 | expect(result.current.todoListSortedStatsValue).toStrictEqual({ "low": 2 }); 148 | 149 | }); 150 | 151 | }); 152 | 153 | describe('SETTERS', () => { 154 | }); -------------------------------------------------------------------------------- /demo-todo/src/store/store.js: -------------------------------------------------------------------------------- 1 | import { selector, selectorFamily } from 'chromogen'; 2 | import { 3 | todoListState, 4 | todoListFilterState, 5 | todoListSortState, 6 | quoteNumberState, 7 | searchResultState, 8 | } from './atoms'; 9 | 10 | /* ----- SELECTORS ---- */ 11 | 12 | // filtered todo list 13 | const filteredTodoListState = selector({ 14 | key: 'filteredTodoListState', 15 | get: ({ get }) => { 16 | const filter = get(todoListFilterState); 17 | const list = get(todoListState); 18 | 19 | switch (filter) { 20 | case 'Show Completed': 21 | return list.filter((item) => item.isComplete); 22 | case 'Show Uncompleted': 23 | return list.filter((item) => !item.isComplete); 24 | default: 25 | return list; 26 | } 27 | }, 28 | }); 29 | 30 | // sorted todo list 31 | const sortedTodoListState = selector({ 32 | key: 'mismatchSortedTodoList', 33 | get: ({ get }) => { 34 | const sort = get(todoListSortState); 35 | const list = get(filteredTodoListState); 36 | const high = list.filter((item) => item.priority === 'high'); 37 | const medium = list.filter((item) => item.priority === 'medium'); 38 | const low = list.filter((item) => item.priority === 'low'); 39 | return sort === false ? list : [...high, ...medium, ...low]; 40 | }, 41 | }); 42 | 43 | // priority stats 44 | const todoListSortedStats = selector({ 45 | key: 'todoListSortedStats', 46 | get: ({ get }) => { 47 | const list = get(sortedTodoListState); 48 | return list.reduce((acc, cv) => { 49 | acc[cv.priority] = cv.priority in acc ? acc[cv.priority] + 1 : 1; 50 | return acc; 51 | }, {}); 52 | }, 53 | }); 54 | 55 | // completion (filter) stats 56 | const todoListStatsState = selector({ 57 | key: 'todoListStatsState', 58 | get: ({ get }) => { 59 | const list = get(todoListState); 60 | const totalNum = list.length; 61 | const totalCompletedNum = list.filter((todo) => todo.isComplete).length; 62 | const totalUncompletedNum = totalNum - totalCompletedNum; 63 | const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum; 64 | return { 65 | totalNum, 66 | totalCompletedNum, 67 | totalUncompletedNum, 68 | percentCompleted, 69 | }; 70 | }, 71 | }); 72 | 73 | // is filtered list non-empty? (determines whether check-all displays) 74 | const filteredListContentState = selector({ 75 | key: 'filteredListContentState', 76 | get: ({ get }) => !!get(filteredTodoListState).length, 77 | }); 78 | 79 | // WRITEABLE GET/SET SELECTOR - (un)check all filtered items 80 | const allCompleteState = selector({ 81 | key: 'mismatchAllComplete', 82 | // if any item in filteredList is not complete, allComplete is false 83 | get: ({ get }) => !get(filteredTodoListState).some(({ isComplete }) => !isComplete), 84 | set: ({ get, set }, newValue) => { 85 | // update ONLY items in filtered list 86 | const lookupTable = {}; 87 | get(todoListState).forEach((item) => { 88 | lookupTable[item.id] = item; 89 | }); 90 | get(filteredTodoListState).forEach((item) => { 91 | lookupTable[item.id] = { 92 | ...item, 93 | isComplete: newValue, 94 | }; 95 | }); 96 | set(todoListState, Object.values(lookupTable)); 97 | }, 98 | }); 99 | 100 | // WRITEABLE RESET SELECTOR - undo sort + filter 101 | const refreshFilterState = selector({ 102 | key: 'refreshFilterState', 103 | get: () => null, 104 | set: ({ reset }) => { 105 | reset(todoListSortState); 106 | reset(todoListFilterState); 107 | }, 108 | }); 109 | 110 | // PROMISE-BASED SELECTOR - fetch quote text 111 | const quoteTextState = selector({ 112 | key: 'quoteTextState', 113 | get: ({ get }) => { 114 | const quoteNumber = get(quoteNumberState); 115 | return fetch('https://type.fit/api/quotes') 116 | .then((response) => response.json()) 117 | .then((data) => { 118 | const quote = data[quoteNumber]; 119 | return `"${quote.text}"\n\t- ${quote.author || 'unknown'}`; 120 | }) 121 | .catch((err) => { 122 | console.error(err); 123 | return 'No quote available'; 124 | }); 125 | }, 126 | }); 127 | 128 | // ASYNC SELECTOR - fetch comic img 129 | // const xkcdState = selector({ 130 | // key: 'xkcdState', 131 | // get: async ({ get }) => { 132 | // const quoteNumber = get(quoteNumberState); 133 | // try { 134 | // // Fetch much be proxied through cors-anywhere to test on localhost 135 | // const response = await fetch( 136 | // `https://cors-anywhere.herokuapp.com/http://xkcd.com/${quoteNumber}/info.0.json`, 137 | // ); 138 | // const { img } = await response.json(); 139 | // return img; 140 | // } catch (err) { 141 | // // Fallback comic 142 | // return 'https://imgs.xkcd.com/comics/api.png'; 143 | // } 144 | // }, 145 | // }); 146 | 147 | const searchBarSelectorFam = selectorFamily({ 148 | key: 'searchBarSelectorFam', 149 | get: 150 | (searchFilter) => 151 | ({ get }) => 152 | get(searchResultState)[searchFilter], 153 | set: 154 | (searchFilter) => 155 | ({ get, set }, searchTerm) => { 156 | set(searchResultState, (prevState) => { 157 | const newResults = get(todoListState).filter((todo) => { 158 | if (searchTerm !== '' && todo.text.includes(searchTerm)) 159 | return searchFilter === 'all' ? true : todo.priority === searchFilter; 160 | return false; 161 | }); 162 | return { ...prevState, [searchFilter]: { searchTerm, results: newResults } }; 163 | }); 164 | }, 165 | }); 166 | 167 | export { 168 | filteredTodoListState, 169 | filteredListContentState, 170 | todoListStatsState, 171 | allCompleteState, 172 | sortedTodoListState, 173 | todoListSortedStats, 174 | refreshFilterState, 175 | quoteTextState, 176 | //xkcdState, 177 | searchBarSelectorFam, 178 | }; 179 | -------------------------------------------------------------------------------- /package/recoil_generator/src/api/api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { 3 | RecoilState, 4 | RecoilValueReadOnly, 5 | AtomOptions, 6 | ReadWriteSelectorOptions, 7 | ReadOnlySelectorOptions, 8 | SerializableParam, 9 | AtomFamilyOptions, 10 | ReadWriteSelectorFamilyOptions, 11 | ReadOnlySelectorFamilyOptions, 12 | } from 'recoil'; 13 | import type { SelectorConfig, SelectorFamilyConfig } from '../types'; 14 | 15 | import { 16 | selector as recoilSelector, 17 | atom as recoilAtom, 18 | atomFamily as recoilAtomFamily, 19 | selectorFamily as recoilSelectorFamily, 20 | } from 'recoil'; 21 | import { wrapGetter, wrapSetter } from './core-utils'; 22 | import { dummyParam } from '../utils/utils'; 23 | import { ledger } from '../utils/ledger'; 24 | import { wrapFamilyGetter, wrapFamilySetter } from './family-utils'; 25 | /* eslint-enable */ 26 | 27 | /** 28 | * If transactions.length is greater than 1, the selector is being created after the initial render 29 | * (i.e. a dynamically generated selector) and will not be tracked. Doing so would break the imports 30 | * and assertions within the output test file. Same logic is applied to new atoms. 31 | * 32 | * If get is undefined, native Async, or Babel-transpiled generator-based async (id'd via RegEx), 33 | * we don't do any injecting or tracking. Selector just gets created & returned back out. 34 | * 35 | * Otherwise, we attempt to wrap get & set methods with custom functions that log the return 36 | * value on each transaction to the corresponding ledger array. 37 | * 38 | * If get returns a promise on page load, we delete selector from the selectors array 39 | * and do not track it on subsequent calls (using "returnedPromise" flag, since we can't "un-inject"). 40 | */ 41 | 42 | /* ----- SELECTOR ----- */ 43 | export function selector(options: ReadWriteSelectorOptions): RecoilState; 44 | export function selector(options: ReadOnlySelectorOptions): RecoilValueReadOnly; 45 | // Overload function signature 46 | export function selector(config: ReadWriteSelectorOptions | ReadOnlySelectorOptions) { 47 | const { key, get } = config; 48 | const { transactions, selectors, setters } = ledger; 49 | if ( 50 | transactions.length > 0 51 | || !get 52 | || get.constructor.name === 'AsyncFunction' 53 | || get.toString().match(/^\s*return\s*_.*\.apply\(this, arguments\);$/m) 54 | ) { 55 | return recoilSelector(config); 56 | } 57 | 58 | // Wrap get method with tracking logic & update config 59 | const getter = wrapGetter(key, get); 60 | const newConfig: SelectorConfig = { key, get: getter }; 61 | 62 | // Add setter to newConfig only if set method is defined 63 | if ('set' in config) { 64 | const setter = wrapSetter(key, config.set); 65 | newConfig.set = setter; 66 | setters.push(key); 67 | } 68 | 69 | // Create selector & add to ledger 70 | const trackedSelector = recoilSelector(newConfig); 71 | selectors.push(trackedSelector.key); 72 | return trackedSelector; 73 | } 74 | 75 | /* ----- ATOM ----- */ 76 | export function atom(config: AtomOptions): RecoilState { 77 | const { transactions, atoms } = ledger; 78 | const newAtom = recoilAtom(config); 79 | 80 | // Can't use key-only b/c atoms must be passed to getLoadable during transaction iteration 81 | if (transactions.length === 0) atoms.push(newAtom); 82 | 83 | return newAtom; 84 | } 85 | 86 | /* ----- ATOM FAMILY ----- */ 87 | export function atomFamily( 88 | config: AtomFamilyOptions, 89 | ): (params: P) => RecoilState { 90 | const { atomFamilies } = ledger; 91 | const { key } = config; 92 | 93 | // Initialize new family in atomFamilies tracker 94 | atomFamilies[key] = {}; 95 | 96 | return (params: P): RecoilState => { 97 | const strParams = JSON.stringify(params); 98 | // If the atom has already been created, return from cache, otherwise we'll be creating a new 99 | // instance of an atom every time we invoke this func (which can lead to infinite re-render loop) 100 | const cachedAtom = atomFamilies[key][strParams]; 101 | if (cachedAtom !== undefined) return cachedAtom; 102 | 103 | const newAtomFamilyMember = recoilAtomFamily(config)(params); 104 | // Storing every atom created except for dummy atom created by ChromogenObserver's onload useEffect hook 105 | if (strParams !== dummyParam) atomFamilies[key][strParams] = newAtomFamilyMember; 106 | return newAtomFamilyMember; 107 | }; 108 | } 109 | 110 | /* ----- SELECTOR FAMILY ----- */ 111 | export function selectorFamily( 112 | options: ReadWriteSelectorFamilyOptions, 113 | ): (param: P) => RecoilState; 114 | export function selectorFamily( 115 | options: ReadOnlySelectorFamilyOptions, 116 | ): (param: P) => RecoilValueReadOnly; 117 | // Overload function signature 118 | export function selectorFamily( 119 | config: 120 | | ReadWriteSelectorFamilyOptions 121 | | ReadOnlySelectorFamilyOptions, 122 | ) { 123 | const { key, get } = config; 124 | const { transactions, selectorFamilies } = ledger; 125 | 126 | // Testing whether returned function from configGet is async 127 | if ( 128 | !get 129 | || transactions.length > 0 130 | || get(dummyParam).constructor.name === 'AsyncFunction' 131 | || get(dummyParam) 132 | .toString() 133 | .match(/^\s*return\s*_.*\.apply\(this, arguments\);$/m) 134 | ) { 135 | return recoilSelectorFamily(config); 136 | } 137 | 138 | const getter = wrapFamilyGetter(key, get); 139 | 140 | const newConfig: SelectorFamilyConfig = { key, get: getter }; 141 | 142 | let isSettable = false; 143 | 144 | if ('set' in config) { 145 | isSettable = true; 146 | const setter = wrapFamilySetter(key, config.set); 147 | newConfig.set = setter; 148 | } 149 | 150 | // Create selector generator & add to selectorFamily for test setup 151 | const trackedSelectorFamily = recoilSelectorFamily(newConfig); 152 | selectorFamilies[key] = { trackedSelectorFamily, prevParams: new Set(), isSettable }; 153 | return trackedSelectorFamily; 154 | } 155 | -------------------------------------------------------------------------------- /demo-zustand-todo/__tests__/sampleTest.js: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import useStore from '../src/store/store'; 3 | 4 | describe('INITIAL RENDER', () => { 5 | const { result } = renderHook(useStore); 6 | 7 | it('todoListState should initialize correctly', () => { 8 | expect(result.current.todoListState).toStrictEqual([]); 9 | }); 10 | 11 | it('todoListFilterState should initialize correctly', () => { 12 | expect(result.current.todoListFilterState).toStrictEqual('Show All'); 13 | }); 14 | 15 | it('todoListSortState should initialize correctly', () => { 16 | expect(result.current.todoListSortState).toStrictEqual(false); 17 | }); 18 | 19 | it('quoteText should initialize correctly', () => { 20 | expect(result.current.quoteText).toStrictEqual(''); 21 | }); 22 | 23 | it('quoteNumber should initialize correctly', () => { 24 | expect(result.current.quoteNumber).toStrictEqual(0); 25 | }); 26 | 27 | it('checkBox should initialize correctly', () => { 28 | expect(result.current.checkBox).toStrictEqual(false); 29 | }); 30 | 31 | it('searchResultState should initialize correctly', () => { 32 | expect(result.current.searchResultState).toStrictEqual({ 33 | all: { searchTerm: '', results: [] }, 34 | high: { searchTerm: '', results: [] }, 35 | medium: { searchTerm: '', results: [] }, 36 | low: { searchTerm: '', results: [] }, 37 | }); 38 | }); 39 | }); 40 | 41 | describe('STATE CHANGES', () => { 42 | const { result } = renderHook(useStore); 43 | 44 | it('checkBox & quoteText & todoListState should update correctly', () => { 45 | const { result } = renderHook(useStore); 46 | 47 | act(() => { 48 | result.current.setCheckBox(); 49 | result.current.setCheckBox(); 50 | result.current.changeQuoteText( 51 | '"Your ability to learn faster than your competition is your only sustainable competitive advantage."\n\t- Arie de Gues', 52 | ); 53 | result.current.addTodoListItem({ id: 2, text: 'tennis', priority: 'low', isComplete: false }); 54 | }); 55 | 56 | expect(result.current.checkBox).toStrictEqual(true); 57 | expect(result.current.quoteText).toStrictEqual( 58 | '"Your ability to learn faster than your competition is your only sustainable competitive advantage."\n\t- Arie de Gues', 59 | ); 60 | expect(result.current.todoListState).toStrictEqual([ 61 | { id: 2, text: 'tennis', priority: 'low', isComplete: false }, 62 | ]); 63 | }); 64 | it('checkBox & todoListState should update correctly', () => { 65 | const { result } = renderHook(useStore); 66 | 67 | act(() => { 68 | result.current.setCheckBox(); 69 | result.current.setCheckBox(); 70 | result.current.addTodoListItem({ id: 3, text: 'hockey', priority: 'low', isComplete: false }); 71 | result.current.setCheckBox(); 72 | }); 73 | 74 | expect(result.current.checkBox).toStrictEqual(false); 75 | expect(result.current.todoListState).toStrictEqual([ 76 | { id: 2, text: 'tennis', priority: 'low', isComplete: false }, 77 | { id: 3, text: 'hockey', priority: 'low', isComplete: false }, 78 | ]); 79 | }); 80 | it('todoListState should update correctly', () => { 81 | const { result } = renderHook(useStore); 82 | 83 | act(() => { 84 | result.current.addTodoListItem({ id: 4, text: 'hocka', priority: 'low', isComplete: false }); 85 | result.current.setCheckBox(); 86 | }); 87 | 88 | expect(result.current.todoListState).toStrictEqual([ 89 | { id: 2, text: 'tennis', priority: 'low', isComplete: false }, 90 | { id: 3, text: 'hockey', priority: 'low', isComplete: false }, 91 | { id: 4, text: 'hocka', priority: 'low', isComplete: false }, 92 | ]); 93 | }); 94 | it('todoListState & searchResultState should update correctly', () => { 95 | const { result } = renderHook(useStore); 96 | 97 | act(() => { 98 | result.current.addTodoListItem({ id: 5, text: 'canoe', priority: 'low', isComplete: false }); 99 | result.current.setCheckBox(); 100 | result.current.setSearchState('c', 'all'); 101 | }); 102 | 103 | expect(result.current.todoListState).toStrictEqual([ 104 | { id: 2, text: 'tennis', priority: 'low', isComplete: false }, 105 | { id: 3, text: 'hockey', priority: 'low', isComplete: false }, 106 | { id: 4, text: 'hocka', priority: 'low', isComplete: false }, 107 | { id: 5, text: 'canoe', priority: 'low', isComplete: false }, 108 | ]); 109 | expect(result.current.searchResultState).toStrictEqual({ 110 | all: { 111 | searchTerm: 'c', 112 | results: [ 113 | { id: 3, text: 'hockey', priority: 'low', isComplete: false }, 114 | { id: 4, text: 'hocka', priority: 'low', isComplete: false }, 115 | { id: 5, text: 'canoe', priority: 'low', isComplete: false }, 116 | ], 117 | }, 118 | high: { searchTerm: '', results: [] }, 119 | medium: { searchTerm: '', results: [] }, 120 | low: { searchTerm: '', results: [] }, 121 | }); 122 | }); 123 | it('searchResultState should update correctly', () => { 124 | const { result } = renderHook(useStore); 125 | 126 | act(() => { 127 | result.current.setSearchState('ca', 'all'); 128 | }); 129 | 130 | expect(result.current.searchResultState).toStrictEqual({ 131 | all: { 132 | searchTerm: 'ca', 133 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], 134 | }, 135 | high: { searchTerm: '', results: [] }, 136 | medium: { searchTerm: '', results: [] }, 137 | low: { searchTerm: '', results: [] }, 138 | }); 139 | }); 140 | it('searchResultState should update correctly', () => { 141 | const { result } = renderHook(useStore); 142 | 143 | act(() => { 144 | result.current.setSearchState('can', 'all'); 145 | }); 146 | 147 | expect(result.current.searchResultState).toStrictEqual({ 148 | all: { 149 | searchTerm: 'can', 150 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], 151 | }, 152 | high: { searchTerm: '', results: [] }, 153 | medium: { searchTerm: '', results: [] }, 154 | low: { searchTerm: '', results: [] }, 155 | }); 156 | }); 157 | it('searchResultState should update correctly', () => { 158 | const { result } = renderHook(useStore); 159 | 160 | act(() => { 161 | result.current.setSearchState('cano', 'all'); 162 | }); 163 | 164 | expect(result.current.searchResultState).toStrictEqual({ 165 | all: { 166 | searchTerm: 'cano', 167 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], 168 | }, 169 | high: { searchTerm: '', results: [] }, 170 | medium: { searchTerm: '', results: [] }, 171 | low: { searchTerm: '', results: [] }, 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /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 | xdescribe('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 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | * { 7 | margin: 0; 8 | } 9 | html, 10 | body, 11 | #root, /* for create-react-app */ 12 | #__next /* for Next.js */ { 13 | height: 100%; 14 | } 15 | body { 16 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Arial, sans-serif; 17 | -webkit-font-smoothing: antialiased; 18 | } 19 | img, 20 | picture, 21 | video, 22 | canvas, 23 | svg { 24 | display: block; 25 | max-width: 100%; 26 | } 27 | input, 28 | button, 29 | textarea, 30 | select { 31 | font: inherit; 32 | } 33 | p, 34 | h1, 35 | h2, 36 | h3, 37 | h4, 38 | h5, 39 | h6 { 40 | overflow-wrap: break-word; 41 | } 42 | #root, 43 | #__next { 44 | isolation: isolate; 45 | } 46 | 47 | /* -------------General Styles---------------- */ 48 | html { 49 | margin: 0; 50 | background-color: #2e3237; 51 | color: #9ee6f7; 52 | overflow-x: hidden; 53 | } 54 | 55 | h1 { 56 | text-align: center; 57 | font-size: 2.3rem; 58 | color: #9ee6f7; 59 | text-shadow: 1px 2px 2px rgba(250, 250, 250, 0.267); 60 | letter-spacing: 3px; 61 | } 62 | input, 63 | button, 64 | select { 65 | background-color: transparent; 66 | /* border: 1px solid lightgray; */ 67 | border: none; 68 | padding: 20px 0; 69 | border-radius: 4px; 70 | color: #9ee6f7; 71 | font-size: 18px; 72 | letter-spacing: 1px; 73 | } 74 | 75 | /* remove browser defaults */ 76 | button:focus { 77 | outline: none; 78 | } 79 | input:focus { 80 | outline: none; 81 | } 82 | /* ------------TodoList------------- */ 83 | 84 | /* topmost container */ 85 | .mainContainer { 86 | display: flex; 87 | flex-direction: column; 88 | height: 100vh; 89 | width: 100vw; 90 | overflow-y: scroll; 91 | } 92 | 93 | .wrapper { 94 | display: flex; 95 | margin: 0 auto; 96 | padding-inline: 32px; 97 | width: 100%; 98 | max-width: 800px; 99 | height: 100%; 100 | flex-direction: column; 101 | padding-bottom: 24px; 102 | } 103 | 104 | /* overall row container for todo list display */ 105 | .todosDisplayRow { 106 | margin: 0 auto; 107 | width: 100%; 108 | height: 100%; 109 | color: #9ee6f7; 110 | } 111 | 112 | a { 113 | color: #9ee6f7; 114 | text-decoration: none; 115 | } 116 | 117 | a:hover { 118 | text-decoration: underline; 119 | } 120 | 121 | .todosDisplayRow h1 { 122 | padding-block: 40px; 123 | } 124 | 125 | /* container for entire list display */ 126 | .todosContainer { 127 | background-color: rgb(255, 255, 255, 0.1); 128 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 129 | border-radius: 5px; 130 | padding: 5px 12px 0 12px; 131 | } 132 | 133 | /* -------------TodoItemCreator---------------- */ 134 | 135 | .itemCreator { 136 | display: flex; 137 | border-bottom: 1px solid rgba(245, 245, 245, 0.336); 138 | } 139 | 140 | .itemCreator button { 141 | padding: 0 24px; 142 | } 143 | 144 | input::placeholder { 145 | letter-spacing: 1.5px; 146 | } 147 | 148 | .itemCreator input { 149 | padding: 0 0 0 12px; 150 | flex-grow: 1; 151 | height: 60px; 152 | } 153 | 154 | #radioContainer svg { 155 | margin-top: 11px; 156 | opacity: 0.7; 157 | } 158 | 159 | label.MuiFormControlLabel-root { 160 | margin-left: 0; 161 | margin-right: 0; 162 | } 163 | 164 | /* -------------TodoItem---------------- */ 165 | 166 | .itemContainer, 167 | .lowPriority, 168 | .mediumPriority, 169 | .highPriority { 170 | display: grid; 171 | grid-template-columns: 79fr 14fr 7fr; 172 | border-bottom: 1px solid rgba(245, 245, 245, 0.336); 173 | } 174 | 175 | /* dynamic checkbox color */ 176 | .highPriority svg { 177 | color: #f10101; 178 | opacity: 0.7; 179 | margin-right: 28px; 180 | } 181 | .mediumPriority svg { 182 | color: #ffe600; 183 | opacity: 0.7; 184 | margin-right: 28px; 185 | } 186 | .lowPriority svg { 187 | color: #05fb11; 188 | opacity: 0.7; 189 | margin-right: 28px; 190 | } 191 | 192 | #todoItem button { 193 | margin-left: 7px; 194 | } 195 | 196 | /* -------------TodoListFilter---------------- */ 197 | 198 | ul { 199 | display: grid; 200 | grid-template-columns: 27fr 27fr 27fr 6fr 6fr 6fr; 201 | margin: 0; 202 | padding: 0px; 203 | width: 100%; 204 | } 205 | 206 | .filter-button { 207 | margin-top: 10px; 208 | margin-bottom: 10px; 209 | border-right: 1px solid rgba(234, 230, 230, 0.26); 210 | padding: 10px 20px; 211 | border-radius: 0px 0px 4px 4px; 212 | } 213 | 214 | /* dynamic sort icon color */ 215 | #sortedWhite svg { 216 | color: whitesmoke; 217 | } 218 | #unsortedGray svg { 219 | color: rgba(245, 245, 245, 0.336); 220 | } 221 | #unsortedGray { 222 | width: 64px; 223 | } 224 | 225 | #statsSpan { 226 | display: grid; 227 | grid-template-columns: 30fr 30fr 30fr; 228 | align-items: center; 229 | } 230 | #highSpan { 231 | color: #ef5350; 232 | opacity: 0.7; 233 | margin-right: 4px; 234 | } 235 | #mediumSpan { 236 | color: #ffee58; 237 | opacity: 0.7; 238 | margin-right: 4px; 239 | } 240 | #lowSpan { 241 | color: #66bb6a; 242 | opacity: 0.7; 243 | } 244 | 245 | /* filter stats (number) */ 246 | button span { 247 | opacity: 0.6; 248 | } 249 | 250 | /* ----------QuoteBox---------- */ 251 | 252 | #quoteContainer { 253 | display: flex; 254 | flex-direction: column; 255 | width: 100%; 256 | /* justify-content: space-between; */ 257 | font-size: 18px; 258 | } 259 | 260 | #quoteContainer p { 261 | white-space: pre-wrap; 262 | padding-bottom: 24px; 263 | } 264 | 265 | .quoteBox { 266 | display: flex; 267 | flex-direction: row; 268 | justify-content: space-between; 269 | border: 1px solid rgba(234, 230, 230, 0.1); 270 | /* background-color: rgb(63, 63, 63); */ 271 | /* box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); */ 272 | border-radius: 5px; 273 | padding: 16px 24px; 274 | } 275 | 276 | .quoteBox img { 277 | margin: 20px; 278 | height: 150px; 279 | width: 150px; 280 | } 281 | 282 | .quoteBox button { 283 | width: 150px; 284 | padding: 24 0; 285 | text-align: left; 286 | } 287 | 288 | .quoteBox button:hover { 289 | cursor: pointer; 290 | /* background-color: rgba(255, 255, 255, 0.1); */ 291 | } 292 | 293 | /* .quoteBox button:active { 294 | background-color: #75acb990; 295 | } */ 296 | 297 | /* ---------- TodoQuickCheck ---------- */ 298 | 299 | /* #quickCheck { 300 | } */ 301 | 302 | #quickCheck svg { 303 | color: #9ee6f7; 304 | } 305 | 306 | button { 307 | cursor: pointer; 308 | } 309 | 310 | /* ---------- Search ---------- */ 311 | .searchContainer { 312 | display: sticky; 313 | margin-top: 100px; 314 | background-color: rgb(255, 255, 255, 0.1); 315 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 316 | border-radius: 5px; 317 | padding: 5px 12px 12px 12px; 318 | display: grid; 319 | grid-template-columns: 70% 30%; 320 | } 321 | 322 | .searchField { 323 | border-bottom: solid 1px whitesmoke; 324 | border-radius: 0; 325 | padding-left: 12px; 326 | } 327 | 328 | .prioritySelect { 329 | grid-column-start: 2; 330 | border-bottom: solid 1px whitesmoke; 331 | border-radius: 0; 332 | -webkit-appearance-select: none; 333 | } 334 | 335 | .searchResults { 336 | grid-column-start: span 3; 337 | } 338 | 339 | #newChromogenLogo { 340 | display: flex; 341 | justify-content: center; 342 | width: 400px; 343 | height: 300px; 344 | } 345 | 346 | select { 347 | -webkit-appearance-select: none; 348 | /* background: url("data:image/svg+xml;utf8,") 349 | no-repeat; */ 350 | } 351 | 352 | /* */ 353 | 354 | .w-tc-editor[data-color-mode*='dark'], 355 | [data-color-mode*='dark'] .w-tc-editor, 356 | [data-color-mode*='dark'] .w-tc-editor-var, 357 | body[data-color-mode*='dark'] { 358 | --color-fg-default: #ddd; 359 | --color-canvas-subtle: #161b22; 360 | --color-prettylights-syntax-comment: #818c97; 361 | --color-prettylights-syntax-entity-tag: #ed81b0; 362 | --color-prettylights-syntax-entity: #d2a8ff; 363 | --color-prettylights-syntax-sublimelinter-gutter-mark: #ddd; 364 | --color-prettylights-syntax-constant: #ed8876; 365 | --color-prettylights-syntax-string: #68afc8; 366 | --color-prettylights-syntax-keyword: #ed81b0; 367 | --color-prettylights-syntax-markup-bold: #c9d1d9; 368 | } 369 | -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 |
2 |

Chromogen

3 | 4 | chromogen logo 10 | 11 | 12 |

A UI-driven Jest test-generation package for Recoil.js selectors and Zustand store hooks.

13 | 14 |
15 | 16 | [![npm version](https://img.shields.io/npm/v/chromogen)](https://www.npmjs.com/package/chromogen) 17 | [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/open-source-labs/Chromogen/blob/master/LICENSE) 18 |
19 | 20 |
21 |
22 | 23 | **Now Compatible with React V18** 24 | 25 | Chromogen (Now on Version 4.0) is a Jest unit-test generation tool for Zustand Stores and 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 can download a ready-to-run Jest test file. Alternatively, you can copy the generated tests straight to your clipboard. 26 |


27 | 28 | ## Installation for Zustand Apps 29 | 30 | Before using Chromogen, you'll need to make two changes to your application: 31 | 32 | 1. Import the `` component and render it alongside any other components in `` 33 | 2. Import `chromogenZustandMiddleware` function from Chromogen. This will be used as middleware when setting up your store. 34 | 35 | ### Import the ChromogenZustandObserver component 36 | 37 | Import `ChromogenZustandObserver`. ChromogenZustandObserver can be rendered alongside any other components in ``. 38 | 39 | ```jsx 40 | import React from 'react'; 41 | import { ChromogenZustandObserver } from 'chromogen'; 42 | import TodoList from './TodoList'; 43 | 44 | const App = () => ( 45 | <> 46 | 47 | 48 | 49 | ); 50 | 51 | export default App; 52 | ``` 53 | 54 | Import `chromogenZustandMiddleware`. When you call create, wrap your store function with chromogenZustandMiddleware. **Note**, when using chromogenZustandMiddleware, you'll need to provide some additional arguments into the set function. 55 | 56 | 1. _Overwrite State_ (boolean) - Without middleware, this defaults to `false`, but you'll need to explicitly provide a value when using Chromogen. 57 | 2. _Action Name_ - Used for test generation 58 | 3. _Action Parameters_ - If the action requires input parameters, pass these in after the Action Name. 59 | 60 | ```jsx 61 | import { chromogenZustandMiddleware } from 'chromogen'; 62 | import create from 'zustand'; 63 | 64 | const useStore = create( 65 | chromogenZustandMiddleware((set) => ({ 66 | counter: 0, 67 | color: 'black', 68 | prioritizeTask: ['walking', 5], 69 | addCounter: () => set(() => ({ counter: (counter += 1) }), false, 'addCounter'), 70 | changeColor: (newColor) => set(() => ({ color: newColor }), false, 'changeColor', newColor), 71 | setTaskPriority: (task, priority) => 72 | set(() => ({ prioritizeTask: [task, priority] }), false, 'setTaskPriority', task, priority), 73 | })), 74 | ); 75 | 76 | export default useStore; 77 | ``` 78 | 79 |

80 | 81 | ## Installation for Recoil Apps 82 | 83 | Before running Chromogen, you'll need to make two changes to your application: 84 | 85 | 1. Import the `` component as a child of `` 86 | 1. Import the `atom` and `selector` functions from Chromogen instead of Recoil 87 | 88 | Note: These changes do have a small performance cost, so they should be reverted before deploying to production. 89 | 90 |
91 | 92 | ### Import the ChromogenObserver component 93 | 94 | 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. 95 | 96 | ```jsx 97 | import React from 'react'; 98 | import { RecoilRoot } from 'recoil'; 99 | import { ChromogenObserver } from 'chromogen'; 100 | import MyComponent from './components/MyComponent.jsx'; 101 | 102 | const App = (props) => ( 103 | 104 | 105 | 106 | 107 | ); 108 | 109 | export default App; 110 | ``` 111 | 112 | 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: 113 | 114 | ```jsx 115 | import * as store from './store'; 116 | // ... 117 | ; 118 | ``` 119 | 120 | If your store utilizes seprate files for various pieces of state, you can pass all of the imports in an array: 121 | 122 | ```jsx 123 | import * as atoms from './store/atoms'; 124 | import * as selectors from './store/selectors'; 125 | import * as misc from './store/arbitraryRecoilState'; 126 | // ... 127 | ; 128 | ``` 129 | 130 |
131 | 132 | ### Import atom & selector functions from Chromogen 133 | 134 | 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. 135 | 136 | ```js 137 | import { atom, selector } from 'chromogen'; 138 | 139 | export const fooState = atom({ 140 | key: 'fooState', 141 | default: {}, 142 | }); 143 | 144 | export const barState = selector({ 145 | key: 'barState', 146 | get: ({ get }) => { 147 | const derivedState = get(fooState); 148 | return derivedState.baz || 'value does not exist'; 149 | }, 150 | }); 151 | ``` 152 | 153 |

154 | 155 | ## Usage for All Apps 156 | 157 | After following the installation steps above, launch your application as normal. You should see two buttons in the bottom left corner. 158 | 159 |
160 | 161 | ![Buttons](https://github.com/open-source-labs/Chromogen/raw/master/assets/README-root/ultratrimmedDemo.gif) 162 | 163 |
164 | 165 | The pause button on the left is the **pause recording** button. Clicking it will pause recording, so that no tests are generated during subsequent state changes. 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. 166 | 167 | The button in the middle 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. 168 | 169 | The button on the right is the **copy-to-clipboard** button. Clicking it will copy your tests, including _all_ tests generated since the app was last launched or refreshed. 170 | 171 | Once you've recorded all the interactions you want to test, click the pause button and then the download button to generate the test file or press copy to copy to your clipboard. You can now drag-and-drop the downloaded file into your app's test directory or paste the code in your new file. **Don't forget to add the source path in your test file** 172 | 173 | You're now ready to run your tests! After running your normal Jest test command, you should see a test suite for `chromogen.test.js`. 174 | 175 | The current tests check whether state has changed after an interaction and checks whether the resulting state change variables have been updated as expected. 176 | 177 |

178 | 179 | Please visit our [main repo](https://github.com/open-source-labs/Chromogen) for more detailed instructions, as well as any bug reports, support issues, or feature requests. 180 | --------------------------------------------------------------------------------
40 |
41 |

Testing!

42 |
71 | 91 | 113 |
114 |