Repository: oslabs-beta/Chromogen Branch: master Commit: 6b2085e43c65 Files: 103 Total size: 229.2 KB Directory structure: gitextract_uv9ye2yw/ ├── .eslintrc.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── pull_request_template.md ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── demo-todo/ │ ├── .babelrc │ ├── LICENSE │ ├── README.md │ ├── __tests__/ │ │ └── initialTestTest.js │ ├── package.json │ ├── src/ │ │ ├── components/ │ │ │ ├── App.jsx │ │ │ ├── Quotes.jsx │ │ │ ├── ReadOnlyTodoItem.jsx │ │ │ ├── SearchBar.jsx │ │ │ ├── TodoItem.jsx │ │ │ ├── TodoItemCreator.jsx │ │ │ ├── TodoList.jsx │ │ │ ├── TodoListFilters.jsx │ │ │ └── TodoQuickCheck.jsx │ │ ├── index.html │ │ ├── index.js │ │ ├── store/ │ │ │ ├── atoms.js │ │ │ └── store.js │ │ └── styles/ │ │ └── styles.css │ └── webpack.config.js ├── demo-zustand-todo/ │ ├── .babelrc │ ├── LICENSE │ ├── __tests__/ │ │ └── sampleTest.js │ ├── chromogen-4.0.4.tgz │ ├── package.json │ ├── src/ │ │ ├── components/ │ │ │ ├── App.jsx │ │ │ ├── Quotes.jsx │ │ │ ├── ReadOnlyTodoItem.jsx │ │ │ ├── SearchBar.jsx │ │ │ ├── TodoItem.jsx │ │ │ ├── TodoItemCreator.jsx │ │ │ ├── TodoList.jsx │ │ │ ├── TodoListFilters.jsx │ │ │ └── TodoQuickCheck.jsx │ │ ├── index.html │ │ ├── index.js │ │ ├── store/ │ │ │ └── store.js │ │ └── styles/ │ │ └── styles.css │ └── webpack.config.js ├── jenkins/ │ ├── Jenkinsfile │ └── scripts/ │ ├── deliver.sh │ ├── kill.sh │ └── test.sh ├── package/ │ ├── LICENSE │ ├── README.md │ ├── babel.config.js │ ├── index.ts │ ├── package.json │ ├── recoil_generator/ │ │ ├── __tests__/ │ │ │ ├── api.test.js │ │ │ ├── component-utils.test.js │ │ │ ├── component.test.js │ │ │ ├── core-utils.test.jx │ │ │ ├── output-utils.test.js │ │ │ ├── output.test.js │ │ │ └── utils.test.js │ │ └── src/ │ │ ├── api/ │ │ │ ├── api.ts │ │ │ ├── core-utils.ts │ │ │ └── family-utils.ts │ │ ├── component/ │ │ │ ├── ChromogenObserver.tsx │ │ │ └── component-utils.ts │ │ ├── output/ │ │ │ ├── output-utils.ts │ │ │ └── output.ts │ │ ├── types.ts │ │ └── utils/ │ │ ├── ledger.ts │ │ ├── store.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── zustand_generator/ │ ├── __tests__/ │ │ ├── api.test.js │ │ └── output-utils.test.js │ └── src/ │ ├── GlobalStyle.ts │ ├── api/ │ │ └── api.ts │ ├── component/ │ │ ├── Buttons/ │ │ │ ├── RecordingButton.tsx │ │ │ ├── RecordingVariations/ │ │ │ │ ├── Record.tsx │ │ │ │ └── Start.tsx │ │ │ └── SecondaryButton.tsx │ │ ├── ChromogenZustandObserver.tsx │ │ ├── Editor.tsx │ │ ├── EditorTab.tsx │ │ ├── Header.tsx │ │ ├── Icons.tsx │ │ ├── Numbers.tsx │ │ ├── Resizing/ │ │ │ └── Resizer.tsx │ │ ├── component-utils.ts │ │ └── panel.tsx │ ├── output/ │ │ ├── output-utils.ts │ │ └── output.ts │ ├── types.ts │ └── utils/ │ ├── ledger.ts │ ├── store.ts │ └── utils.ts └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "root": true, "parser": "@typescript-eslint/parser", "env": { "browser": true, "es2020": true, "jest": true }, "globals": { "chrome": "readonly" }, "extends": [ "airbnb", // added below for npm install -D eslint-config-airbnb-typescript --legacy-peer-deps "airbnb-typescript", "prettier", "prettier/react" ], "plugins": ["prettier"], "parserOptions": { "ecmaVersion": 2022, "sourceType": "module", "ecmaFeatures": { "jsx": true }, "project": "./package/tsconfig.json", "tsconfigRootDir": "__dirname" }, "settings": { "import/extensions": [ ".js", ".jsx", ".ts", ".tsx" ], "import/parsers": { "@typescript-eslint/parser": [ ".ts", ".tsx" ] }, "import/resolver": { "typescript": { "directory": "./package/tsconfig.json" }, "node": { "extensions": [ ".js", ".jsx", ".ts", ".tsx" ] } }, "rules": { "prettier/prettier": ["warn"], "arrow-body-style": ["error", "as-needed"], "default-case-last": "error", "default-param-last": ["error"], "func-style": ["off", "expression"], "no-constant-condition": "error", "no-useless-call": "error", "prefer-exponentiation-operator": "error", "prefer-regex-literals": "error", "quotes": [ "error", "single", { "avoidEscape": true, "allowTemplateLiterals": false } ], "import/prefer-default-export": "off", "import/extensions": [ "error", "ignorePackages", { "js": "never", "jsx": "never", "ts": "never", "tsx": "never" } ], "react/jsx-filename-extension": ["off"], "react/function-component-definition": [ "error", { "namedComponents": "arrow-function", "unnamedComponents": "arrow-function" } ], "react/jsx-handler-names": [ "error", { "eventHandlerPrefix": "handle", "eventHandlerPropPrefix": "on" } ], "react/jsx-key": "error", "react/jsx-no-useless-fragment": "error", "react/jsx-sort-props": [ "error", { "callbacksLast": true, "shorthandFirst": true, "shorthandLast": false, "ignoreCase": true, "noSortAlphabetically": false, "reservedFirst": true } ], "react/no-adjacent-inline-elements": "error", "react/no-direct-mutation-state": "error", "react/no-multi-comp": "error", "react/prop-types": [ "error", { "skipUndeclared": true } ] } } } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "npm" directory: "/package" schedule: interval: "weekly" ================================================ FILE: .github/pull_request_template.md ================================================ ## Types of changes - [ ] Bugfix (change which fixes an issue) - [ ] New feature (change which adds functionality) - [ ] Refactor (change which changes the codebase without affecting its external behavior) - [ ] Non-breaking change (fix or feature that would causes existing functionality to work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to __not__ work as expected) ## Purpose ## Approach ## Resources ## Screenshot(s) ================================================ FILE: .gitignore ================================================ # npm node_modules # System files .DS_Store .vscode # Tests coverage # Package package/build # Demo App chromogen.test.js package-lock.json TODO.md ================================================ FILE: .prettierrc.json ================================================ { "printWidth": 100, "tabWidth": 2, "useTabs": false, "semi": true, "singleQuote": true, "quoteProps": "as-needed", "jsxSingleQuote": false, "trailingComma": "all", "bracketSpacing": true, "jsxBracketSameLine": false, "arrowParens": "always", "endOfLine": "lf" } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 14 env: - TEST_DIR=package before_install: - cd $TEST_DIR install: - npm install script: - npm run test - npm run coveralls ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at chromogen.app@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: Dockerfile ================================================ FROM jenkins/jenkins:2.375.3 USER root RUN apt-get update && apt-get install -y lsb-release RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \ https://download.docker.com/linux/debian/gpg RUN echo "deb [arch=$(dpkg --print-architecture) \ signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \ https://download.docker.com/linux/debian \ $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list RUN apt-get update && apt-get install -y docker-ce-cli USER jenkins RUN jenkins-plugin-cli --plugins "blueocean docker-workflow" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 OSLabs Beta Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
Chromogen logo

A UI-driven test-generation package for Zustand Stores and Recoil.js selectors.


[![npm version](https://img.shields.io/npm/v/chromogen)](https://www.npmjs.com/package/chromogen) [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/open-source-labs/Chromogen/blob/master/LICENSE) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=CHROMOGEN%20-%20A%20UI-driven%20Jest%20test%20generator%20for%20Recoil%20apps%0A&url=https://www.npmjs.com/package/Chromogen&hashtags=React,Recoil,Jest,testing) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) [![npm downloads](https://img.shields.io/npm/dm/chromogen)](https://www.npmjs.com/package/chromogen) [![Github stars](https://img.shields.io/github/stars/open-source-labs/Chromogen?style=social)](https://github.com/open-source-labs/Chromogen)

## Table of Contents - [Overview](#overview) - [Supported Tests](#supported-tests) - [Installing the Package](#installing-the-package) - [Installation for Zustand Apps](#installation-for-zustand-apps) - [Installation for Recoil Apps](#installation-for-recoil-apps) - [Usage for All Apps](#usage-for-all-apps) - [Contributor Setup](#contributor-setup) - [Test Setup](#test-setup) - [CI/CD with Jenkins](#jenkins) - [Demo Apps](#demo-apps) - [Contributing](#contributing) - [Core Team](#core-team) - [License](#license)

## Overview You're an independent developer or part of a lean team. You want reliable unit tests for your new Zustand or React-Recoil app, but you need to move fast and time is a major constraint. More importantly, you want your tests to reflect how your users interact with the application, rather than testing implementation details.

[Enter Chromogen - Now on version 4.0](https://www.npmjs.com/package/chromogen). Chromogen 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. Chromogen is now compatible with React V18!

## Supported Tests Zustand Tests Chromogen currently supports two types of testing for Zustand applications: 1. **Initial Store State** on page load. 2. **Store State Changes** whenever an action is invoked on the store. On initial render, Chromogen captures store state as a whole and keeps track of any subsequent state changes. In order to generate tests, you'll need to make some changes to how your store is created. To use Chromogen with your Zustand application, please see the [Installation for Zustand Apps](#installation-for-zustand-apps) section below.
Recoil Tests Chromogen currently supports three main types of tests for Recoil apps: 1. **Initial selector values** on page load 2. **Selector return values** for a given state, using snapshots captured after each state transaction. 3. **Selector _set_ logic** asserting on resulting atom values for a given `newValue` argument and starting state. These test suites will be captured for _synchronous_ selectors and selectorFamilies only. However, the presence of asyncronous selectors in your app should not cause any issues with the generated tests. Chromogen can identify such selectors at run-time and exclude them from capture. At this time, we have no plans to introduce testing for async selectors; the mocking requirements are too opaque and fragile to accurately capture at runtime. By default, Chromogen uses atom and selector keys to populate the import & hook statements in the test file. If your source code does _not_ use matching variable and key names, you will need to pass the imported atoms and selectors to the Chromogen component as a `store` prop. The installation instructions below contain further details.

## Installing the Package ``` npm install chromogen ```

## Installation for Zustand Apps Before using Chromogen, you'll need to make two changes to your application: 1. Import the `` component and render it alongside any other components in `` 2. Import `chromogenZustandMiddleware` function from Chromogen. This will be used as middleware when setting up your store. ### Import the ChromogenZustandObserver component Import `ChromogenZustandObserver`. ChromogenZustandObserver can be rendered alongside any other components in ``. ```jsx import React from 'react'; import { ChromogenZustandObserver } from 'chromogen'; import TodoList from './TodoList'; const App = () => ( <> ); export default App; ``` 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. 1. _Overwrite State_ (boolean) - Without middleware, this defaults to `false`, but you'll need to explicitly provide a value when using Chromogen. 2. _Action Name_ - Used for test generation 3. _Action Parameters_ - If the action requires input parameters, pass these in after the Action Name. ```jsx import { chromogenZustandMiddleware } from 'chromogen'; import create from 'zustand'; const useStore = create( chromogenZustandMiddleware((set) => ({ counter: 0, color: 'black', prioritizeTask: ['walking', 5], addCounter: () => set(() => ({ counter: (counter += 1) }), false, 'addCounter'), changeColor: (newColor) => set(() => ({ color: newColor }), false, 'changeColor', newColor), setTaskPriority: (task, priority) => set(() => ({ prioritizeTask: [task, priority] }), false, 'setTaskPriority', task, priority), })), ); export default useStore; ```

## Installation for Recoil Apps Before running Chromogen, you'll need to make two changes to your application: 1. Import the `` component as a child of `` 1. Import the `atom` and `selector` functions from Chromogen instead of Recoil Note: These changes do have a small performance cost, so they should be reverted before deploying to production.
### Import the ChromogenObserver component 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. ```jsx import React from 'react'; import { RecoilRoot } from 'recoil'; import { ChromogenObserver } from 'chromogen'; import MyComponent from './components/MyComponent.jsx'; const App = (props) => ( ); export default App; ``` 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: ```jsx import * as store from './store'; // ... ; ``` If your store utilizes seprate files for various pieces of state, you can pass all of the imports in an array: ```jsx import * as atoms from './store/atoms'; import * as selectors from './store/selectors'; import * as misc from './store/arbitraryRecoilState'; // ... ; ```
### Import atom & selector functions from Chromogen 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. ```js import { atom, selector } from 'chromogen'; export const fooState = atom({ key: 'fooState', default: {}, }); export const barState = selector({ key: 'barState', get: ({ get }) => { const derivedState = get(fooState); return derivedState.baz || 'value does not exist'; }, }); ```

## Usage for All Apps After following the installation steps above, launch your application as normal. You should see two buttons in the bottom left corner.
![Buttons](./assets/README-root/ultratrimmedDemo.gif)
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. 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. 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. 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** 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`. The current tests check whether state has changed after an interaction and checks whether the resulting state change variables have been updated as expected.

## Contributor Setup In order to make/observe changes to the code, you'll have to run Chromogen locally as opposed to running via NPM. Due to inconsistencies across different machines, it is recomended to use the following method to run Chromogen locally.

**Run for demos within this directory** After cloning the repo, ```jsx npm install ``` from BOTH the /package directory (where the app lives) AND the /demo directory you're developing with. Then, and ONLY then, run ```jsx npm run tarballUpdate ```

**Run for local applications outside this directory** After cloning this repo, add the following script to your app's package.json: ```jsx "tarballUpdate": "npm --prefix run build && npm pack && npm uninstall chromogen && npm install ./chromogen-5.0.1.tgz && npm start" ```

## Test Setup ### Zustand Test Setup Before running the test file, you'll need to specify the import path for your store by replacing ``. The default output assumes that all stores are imported from a single path; if that's not possible, you'll need to separately import each set of stores from their appropriate path. | **BEFORE** | **AFTER** | | :-------------------------------------------------------------------: | :-------------------------------------------------------------------: | | ![Default Filepath](./assets/README-root/zustand-test-filepath-1.png) | ![Updated Filepath](./assets/README-root/zustand-test-filepath-2.png) |
![Test Output](./assets/README-root/zustand-test-snapshot-2.png)

--- ### Recoil Test Setup Before running the test file, you'll need to specify the import path for your store by replacing ``. The default output assumes that all atoms and selectors are imported from a single path; if that's not possible, you'll need to separately import each set of atoms and/or selectors from their appropriate path. | **BEFORE** | **AFTER** | | :-----------------------------------------------------------: | :----------------------------------------------------------: | | ![Default Filepath](./assets/README-root/filepath-before.png) | ![Updated Filepath](./assets/README-root/filepath-after.png) | You're now ready to run your tests! Upon running your normal Jest test command, you should see three suites for `chromogen.test.js`:
![Test Output](./assets/README-root/test-output.png)
**Initial Render** tests whether each selector returns the correct value at launch. There is one test per selector. **Selectors** tests the return value of various selectors for a given state. Each test represents the app state after a transaction has occured, generally triggered by some user interaction. For each selector that ran after that transaction, the test asserts on the selector's return value for the given state. **Setters** tests the state that results from setting a writeable selector with a given value and starting state. There is one test per set call, asserting on each atom's value in the resulting state.

## CI/CD with Jenkins You will need to have Docker installed to run Jenkins. Ru the following command to create a bridge network for Jenkins: ```jsx docker network create jenkins ``` Enable Docker commands to be executable with Jenkins nodes: ```jsx docker run \ --name jenkins-docker \ --rm \ --detach \ --privileged \ --network jenkins \ --network-alias docker \ --env DOCKER_TLS_CERTDIR=/certs \ --volume jenkins-docker-certs:/certs/client \ --volume jenkins-data:/var/jenkins_home \ --publish 2376:2376 \ --publish 3003:3003 --publish 5003:5003 \ docker:dind \ --storage-driver overlay2 ``` Build a Docker image from the DOckerfile within Chromogen: ```jsx docker build -t chromogen-jenkins . ``` Run your Chromogen-Jenkins image in Docker as a container: ```jsx docker run \ --name jenkins-blueocean \ --detach \ --network jenkins \ --env DOCKER_HOST=tcp://docker:2376 \ --env DOCKER_CERT_PATH=/certs/client \ --env DOCKER_TLS_VERIFY=1 \ --publish 8080:8080 \ --publish 50000:50000 \ --volume jenkins-data:/var/jenkins_home \ --volume jenkins-docker-certs:/certs/client:ro \ --volume "$HOME":/home \ --restart=on-failure \ --env JAVA_OPTS="-Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true" \ myjenkins-blueocean:2.375.3-1 ``` Navigate to your localhost:8080 and enter the password (between the two sets of asterisks) that is generated from the following command: ```jsx docker logs jenkins-blueocean ``` Create your first administrator user. Stop and start your Docker container using one of the following: ```jsx docker stop jenkins-blueocean jenkins-docker docker start jenkins-blueocean jenkins-docker ``` When configuring your pipeline, make sure to set your pipeline definition to 'Pipeline Script from SCM' and enter the path to your local repositiory, specify the brand you are working from, set the Script Path to 'jenkins/Jenkinsfile', and uncheck 'Lightweight Checkout'. ## Demo Apps ### Zustand Demo To-Do App Chromogen's open-source Zustand demo app provides a Zustand-based frontend with multiple store properties and actions to test. You can access this demo application here, and view the source code in the `demo-zustand-todo` folder of this repository.
### Recoil Demo To-Do App Chromogen's official Recoil demo app provides a ready-to-run Recoil frontend with a number of different selector implementations to test against. It's available in the `demo-todo` folder of this repository and comes with Chromogen pre-installed; just run `npm install && npm start` to launch.

## Contributing **We expect all contributors to abide by the standards of behavior outlined in the [Code of Conduct](CODE_OF_CONDUCT.md).** We welcome community contributions, including new developers who've never [made an open source Pull Request before](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). If you'd like to start a new PR, we recommend [creating an issue](https://docs.github.com/en/github/managing-your-work-on-github/creating-an-issue) for discussion first. This lets us open a conversation, ensuring work is not duplicated unnecessarily and that the proposed PR is a fix or feature we're actively looking to add. ## Bugs Please [file an issue](https://docs.github.com/en/github/managing-your-work-on-github/creating-an-issue) for bugs, missing documentation, or unexpected behavior. ## Feature Requests Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps us prioritize what to work on. ## Questions For any questions and concerns related to using the package, feel free to email us via `chromogen.app@gmail.com`.

## Chromogen V5.0 updates

**GUI Overhaul** **Why?** *Hovering GUI blocked functionality of host app *Recording/downloading interactivity was cumbersome and inflexible *Suboptimal for CI/CD implementation Buttons not functional **What?** *Discrete Collapsible IDE that allows for real-time observation & manual interactivity of generated tests **Next steps:** *Recording button functionality



**Real-time feed rendering** **Why?** *Generated tests were only accessible as a monolith of text, preventing isolation of individual components’ tests **What?** *IDE updates in real-time as changes of state are recorded **Next steps:** *Test categorization. *Filter groups of tests by: *Initialization vs ΔState Action *Description *and allow user to select which filter to apply to displayed generated tests.



**CI/CD overhaul** **Why?** *Travis implementation not functional **What?** *Re-implemented CI/CD with Jenkins



**Additional Next Steps** **Add functionality for Zustand multi-store rendering & Asynchronous state** **Docker containerization** **Why?** *V 4.0 presented inconsistencies when accessed from different local machines. This hindered team workflow both with development and production-use **What?** *Containerization of app ensures homogenous, improved User/Dev experience



## Core Team

Brach Burdick

Francois Denavaut

Maggie Kwan

Lawrence Liang

Michelle Holland

Andy Wang

Connor Rose Delisle

Jim Chen

Amy Yee

Jinseon Shin

Ryan Tumel

Cameron Greer

Nicholas Shay

Marcellies Pettiford

Sung Kim

Lina Lee

Erica Oh

Dani Almaraz

Craig Boswell

Hussein Ahmed

Ian Kila

Yuehao Wong


## LICENSE Logo crafted with [AdobeExpress](https://www.adobe.com/express/) README format adapted from [react-testing-library](https://github.com/testing-library/react-testing-library/blob/master/README.md) under [MIT license](https://github.com/testing-library/react-testing-library/blob/master/LICENSE). All Chromogen source code is [MIT](./LICENSE) licensed. Lastly, shoutout to [this repo](https://github.com/conorhastings/redux-test-recorder) for the original inspiration. ================================================ FILE: demo-todo/.babelrc ================================================ { "presets": [ ["@babel/preset-env"], "@babel/preset-react" ], "plugins": [ //"react-hot-loader/babel" ] } ================================================ FILE: demo-todo/LICENSE ================================================ MIT License Copyright (c) 2020 Michelle Holland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: demo-todo/README.md ================================================
# The official demo app for [Chromogen](https://github.com/oslabs-beta/Chromogen). ![demo app interface](../assets/README-demo/demo-app.png)
## Selector Implementations - Readonly: 1. displayed todo list items, based on filter selection (sort & active/complete) 2. stats (priority count and active/complete counts) 3. displayed todo list empty / non-empty - Writeable: 1. "all complete" checkbox toggle 1. reset filter states - Promise: 1. quote text - Async / Await: 1. xkcd comic - selectorFamily (_in progress_): 1. search bar ================================================ FILE: demo-todo/__tests__/initialTestTest.js ================================================ import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library'; import { useRecoilValue, useRecoilState } from 'recoil'; import { filteredTodoListState, sortedTodoListState, todoListSortedStats, todoListStatsState, filteredListContentState, allCompleteState, refreshFilterState, searchBarSelectorFam, } from '../src/store/store'; import { todoListState, todoListFilterState, todoListSortState, quoteNumberState, searchResultState, } from '../src/store/atoms'; // Suppress 'Batcher' warnings from React / Recoil conflict console.error = jest.fn(); // Hook to return atom/selector values and/or modifiers for react-recoil-hooks-testing-library const useStoreHook = () => { // atoms const [todoListStateValue, settodoListState] = useRecoilState(todoListState); const [todoListFilterStateValue, settodoListFilterState] = useRecoilState(todoListFilterState); const [todoListSortStateValue, settodoListSortState] = useRecoilState(todoListSortState); const [quoteNumberStateValue, setquoteNumberState] = useRecoilState(quoteNumberState); const [searchResultStateValue, setsearchResultState] = useRecoilState(searchResultState); // writeable selectors const [allCompleteStateValue, setallCompleteState] = useRecoilState(allCompleteState); const [refreshFilterStateValue, setrefreshFilterState] = useRecoilState(refreshFilterState); // read-only selectors const filteredTodoListStateValue = useRecoilValue(filteredTodoListState); const sortedTodoListStateValue = useRecoilValue(sortedTodoListState); const todoListSortedStatsValue = useRecoilValue(todoListSortedStats); const todoListStatsStateValue = useRecoilValue(todoListStatsState); const filteredListContentStateValue = useRecoilValue(filteredListContentState); // atom families // writeable selector families // read-only selector families return { todoListStateValue, settodoListState, todoListFilterStateValue, settodoListFilterState, todoListSortStateValue, settodoListSortState, quoteNumberStateValue, setquoteNumberState, searchResultStateValue, setsearchResultState, allCompleteStateValue, setallCompleteState, refreshFilterStateValue, setrefreshFilterState, filteredTodoListStateValue, sortedTodoListStateValue, todoListSortedStatsValue, todoListStatsStateValue, filteredListContentStateValue, }; }; describe('INITIAL RENDER', () => { const { result } = renderRecoilHook(useStoreHook); it('filteredTodoListState should initialize correctly', () => { expect(result.current.filteredTodoListStateValue).toStrictEqual([]); }); it('sortedTodoListState should initialize correctly', () => { expect(result.current.sortedTodoListStateValue).toStrictEqual([]); }); it('allCompleteState should initialize correctly', () => { expect(result.current.allCompleteStateValue).toStrictEqual(true); }); it('filteredListContentState should initialize correctly', () => { expect(result.current.filteredListContentStateValue).toStrictEqual(false); }); it('todoListSortedStats should initialize correctly', () => { expect(result.current.todoListSortedStatsValue).toStrictEqual({}); }); it('todoListStatsState should initialize correctly', () => { expect(result.current.todoListStatsStateValue).toStrictEqual({ "totalNum": 0, "totalCompletedNum": 0, "totalUncompletedNum": 0, "percentCompleted": 0 }); }); }); describe('SELECTORS', () => { it('todoListSortedStats should properly derive state when todoListState updates', () => { const { result } = renderRecoilHook(useStoreHook); act(() => { result.current.settodoListState([{ "id": 1, "text": "tennis", "priority": "low", "isComplete": false }]); result.current.settodoListFilterState("Show All"); result.current.settodoListSortState(false); result.current.setquoteNumberState(23); result.current.setsearchResultState({ "all": { "searchTerm": "", "results": [] }, "high": { "searchTerm": "", "results": [] }, "medium": { "searchTerm": "", "results": [] }, "low": { "searchTerm": "", "results": [] } }); }); expect(result.current.todoListSortedStatsValue).toStrictEqual({ "low": 1 }); }); it('todoListSortedStats should properly derive state when todoListState updates', () => { const { result } = renderRecoilHook(useStoreHook); act(() => { result.current.settodoListState([{ "id": 1, "text": "tennis", "priority": "low", "isComplete": false }, { "id": 2, "text": "chinese chicken", "priority": "low", "isComplete": false }]); result.current.settodoListFilterState("Show All"); result.current.settodoListSortState(false); result.current.setquoteNumberState(23); result.current.setsearchResultState({ "all": { "searchTerm": "", "results": [] }, "high": { "searchTerm": "", "results": [] }, "medium": { "searchTerm": "", "results": [] }, "low": { "searchTerm": "", "results": [] } }); }); expect(result.current.todoListSortedStatsValue).toStrictEqual({ "low": 2 }); }); }); describe('SETTERS', () => { }); ================================================ FILE: demo-todo/package.json ================================================ { "name": "chromogen-todo", "version": "1.0.0", "description": "demo todo app for Chromogen using React + Recoil", "main": "index.js", "scripts": { "start": "webpack-dev-server --open", "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --verbose" }, "keywords": [ "react", "recoil", "chromogen", "demo", "example", "todo" ], "repository": { "type": "git", "url": "https://github.com/open-source-labs/Chromogen.git" }, "contributors": [ { "name": "Michelle Holland", "url": "https://github.com/michellebholland/" }, { "name": "Jim Chen", "url": "https://github.com/chenchingk" }, { "name": "Andy Wang", "url": "https://github.com/andywang23" }, { "name": "Connor Rose Delisle", "url": "https://github.com/connorrose" } ], "license": "MIT", "devDependencies": { "@babel/core": "^7.11.1", "@babel/plugin-transform-runtime": "^7.11.0", "@babel/preset-env": "^7.19.1", "@babel/preset-react": "^7.10.4", "@testing-library/react": "^13.1.1", "babel-loader": "^8.1.0", "chromogen": "^4.0.4", "css-loader": "^4.2.1", "identity-obj-proxy": "^3.0.0", "jest": "^26.4.2", "style-loader": "^1.2.1", "webpack": "^5.74.0", "webpack-cli": "^3.3.12", "webpack-dev-server": "^4.11.1" }, "peerDependencies": { "typescript": "^4.0.3" }, "dependencies": { "@babel/runtime": "^7.11.2", "@emotion/react": "^11.10.4", "@emotion/styled": "^11.10.4", "@mui/icons-material": "^5.10.6", "@mui/material": "^5.10.6", "babel-jest": "^26.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-recoil-hooks-testing-library": "^0.1.0", "react-test-renderer": "^18.1.0", "recoil": "0.7.2", "typescript": "^4.0.3" }, "jest": { "moduleNameMapper": { "\\.(css|less)$": "identity-obj-proxy" } } } ================================================ FILE: demo-todo/src/components/App.jsx ================================================ import React from 'react'; import { RecoilRoot } from 'recoil'; import { ChromogenObserver } from 'chromogen'; import TodoList from './TodoList'; import * as selectors from '../store/store'; import * as atoms from '../store/atoms'; const App = () => ( ); export default App; ================================================ FILE: demo-todo/src/components/Quotes.jsx ================================================ import React from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { quoteTextState, xkcdState } from '../store/store'; import { quoteNumberState } from '../store/atoms'; const Quotes = () => { const setQuoteNumber = useSetRecoilState(quoteNumberState); const quoteText = useRecoilValue(quoteTextState); return ( <>

{quoteText}

); }; export default Quotes; ================================================ FILE: demo-todo/src/components/ReadOnlyTodoItem.jsx ================================================ import React from 'react'; import Checkbox from '@mui/material/Checkbox'; import '../styles/styles.css'; import { todoListState } from '../store/atoms'; import { useRecoilValue } from 'recoil'; const ReadOnlyTodoItem = ({ item }) => { const checkBoxClasses = { low: 'lowPriority', medium: 'mediumPriority', high: 'highPriority', }; const todoList = useRecoilValue(todoListState); return todoList.find((todo) => todo.id === item.id) ? (
todo.id === item.id).isComplete} color="default" inputProps={{ 'aria-label': 'primary checkbox' }} style={{ cursor: 'default' }} />
) : null; }; export default ReadOnlyTodoItem; ================================================ FILE: demo-todo/src/components/SearchBar.jsx ================================================ import React, { useState } from 'react'; import { useRecoilState } from 'recoil'; import { searchBarSelectorFam } from '../store/store'; import ReadOnlyTodoItem from './ReadOnlyTodoItem'; const SearchBar = () => { const [searchFilter, setSearchFilter] = useState('all'); const [searchText, setSearchText] = useState(''); const [searchState, setSearchState] = useRecoilState(searchBarSelectorFam(searchFilter)); const onSearchTextChange = (e) => { setSearchText(e.target.value); setSearchState(e.target.value); }; const onSelectChange = (e) => { setSearchText(''); setSearchFilter(e.target.value); }; return (
{searchState.results.map((result, idx) => ( ))}
); }; export default SearchBar; ================================================ FILE: demo-todo/src/components/TodoItem.jsx ================================================ import React from 'react'; import { useRecoilState } from 'recoil'; import Checkbox from '@mui/material/Checkbox'; import { todoListState } from '../store/atoms'; import '../styles/styles.css'; function replaceItemAtIndex(arr, index, newValue) { return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; } function removeItemAtIndex(arr, index) { return [...arr.slice(0, index), ...arr.slice(index + 1)]; } const TodoItem = ({ item }) => { const [todoList, setTodoList] = useRecoilState(todoListState); const index = todoList.findIndex((listItem) => listItem === item); const editItemText = ({ target: { value } }) => { const newList = replaceItemAtIndex(todoList, index, { ...item, text: value, }); setTodoList(newList); }; const toggleItemCompletion = () => { const newList = replaceItemAtIndex(todoList, index, { ...item, isComplete: !item.isComplete, }); setTodoList(newList); }; const deleteItem = () => { const newList = removeItemAtIndex(todoList, index); setTodoList(newList); }; const checkBoxClasses = { low: 'lowPriority', medium: 'mediumPriority', high: 'highPriority', }; return (
); }; export default TodoItem; ================================================ FILE: demo-todo/src/components/TodoItemCreator.jsx ================================================ /* eslint-disable react/jsx-props-no-spreading */ import React, { useState } from 'react'; import RadioGroup from '@mui/material/RadioGroup'; import FormControlLabel from '@mui/material/FormControlLabel'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; import Radio from '@mui/material/Radio'; import { useSetRecoilState } from 'recoil'; import { todoListState } from '../store/atoms'; // utility for creating unique Id let id = 0; const getId = () => { id += 1; return id; }; const TodoItemCreator = () => { const [inputValue, setInputValue] = useState(''); const [priorityValue, setPriorityValue] = useState('low'); const setTodoList = useSetRecoilState(todoListState); const addItem = () => { setTodoList((oldTodoList) => [ ...oldTodoList, { id: getId(), text: inputValue, priority: priorityValue, isComplete: false, }, ]); setInputValue(''); setPriorityValue('low'); }; const onChange = ({ target: { value } }) => { setInputValue(value); }; const handleChange = (event) => { setPriorityValue(event.target.value); }; /* MUI Radio Button styles */ const GreenRadio = (props) => ; const YellowRadio = (props) => ; const RedRadio = (props) => ; return (
} value="high" /> } value="medium" /> } value="low" />
); }; export default TodoItemCreator; ================================================ FILE: demo-todo/src/components/TodoList.jsx ================================================ import React from 'react'; import { useRecoilValue } from 'recoil'; import { sortedTodoListState } from '../store/store'; import TodoItem from './TodoItem'; import TodoItemCreator from './TodoItemCreator'; import TodoListFilters from './TodoListFilters'; import TodoQuickCheck from './TodoQuickCheck'; import Quotes from './Quotes'; import SearchBar from './SearchBar'; import '../styles/styles.css'; const TodoList = () => { const todoList = useRecoilValue(sortedTodoListState); return (
Loading...}>

Totally Todos!

{todoList.map((todoItem) => ( ))}
); }; export default TodoList; ================================================ FILE: demo-todo/src/components/TodoListFilters.jsx ================================================ import React, { useState } from 'react'; import SortIcon from '@mui/icons-material/Sort'; import EqualizerIcon from '@mui/icons-material/Equalizer'; import RefreshIcon from '@mui/icons-material/Refresh'; import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'; import { todoListStatsState, todoListSortedStats, refreshFilterState } from '../store/store'; import { todoListFilterState, todoListSortState } from '../store/atoms'; const TodoListFilters = () => { const [filter, setFilter] = useRecoilState(todoListFilterState); // selector - grabs totals for each category const { high, medium, low } = useRecoilValue(todoListSortedStats); // selector *writeable - resets sort and filter const resetFilters = useResetRecoilState(refreshFilterState); // selector - toggles sort on and off const [sort, setSort] = useRecoilState(todoListSortState); // toggle priority stats display const [displayStats, setDisplayStats] = useState(false); // selector - totals for each filter const { totalNum, totalCompletedNum, totalUncompletedNum } = useRecoilValue(todoListStatsState); const updateFilter = ({ target: { value } }) => setFilter(value); const toggleSort = () => setSort(!sort); const toggleDisplayStats = () => setDisplayStats(!displayStats); const reset = () => { setDisplayStats(false); // displayStats is local state resetFilters(); }; const sortIconColor = { true: 'sortedWhite', false: 'aqua', }; return (
); }; export default TodoListFilters; ================================================ FILE: demo-todo/src/components/TodoQuickCheck.jsx ================================================ import React from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import Checkbox from '@mui/material/Checkbox'; import { allCompleteState, filteredListContentState } from '../store/store'; const TodoQuickCheck = () => { const [allComplete, setAllComplete] = useRecoilState(allCompleteState); const display = useRecoilValue(filteredListContentState); return ( display && (
setAllComplete(!allComplete)} /> all
) ); }; export default TodoQuickCheck; ================================================ FILE: demo-todo/src/index.html ================================================ Chromogen To-Do Demo
================================================ FILE: demo-todo/src/index.js ================================================ /* eslint-disable react/jsx-filename-extension */ import React from 'react'; //import { render } from 'react-dom'; import App from './components/App'; import { createRoot } from'react-dom/client'; const root = createRoot(document.getElementById('app')); root.render( ); ================================================ FILE: demo-todo/src/store/atoms.js ================================================ import { atom } from 'chromogen'; /* ----- ATOMS ----- */ // unsorted, unfiltered todo list const todoListState = atom({ key: 'mismatchTodoList', default: [], // array of objects - each object has id, text, isComplete, and priority props }); // filter select const todoListFilterState = atom({ key: 'todoListFilterState', default: 'Show All', }); // toggle sort const todoListSortState = atom({ key: 'todoListSortState', default: false, }); // random number for fetching quote & comic const quoteNumberState = atom({ key: 'quoteNumberState', default: Math.floor(Math.random() * 1643), }); const searchResultState = atom({ key: 'searchResultState', default: { all: { searchTerm: '', results: [], }, high: { searchTerm: '', results: [], }, medium: { searchTerm: '', results: [], }, low: { searchTerm: '', results: [], }, }, }); export { todoListState, todoListFilterState, todoListSortState, quoteNumberState, searchResultState, }; ================================================ FILE: demo-todo/src/store/store.js ================================================ import { selector, selectorFamily } from 'chromogen'; import { todoListState, todoListFilterState, todoListSortState, quoteNumberState, searchResultState, } from './atoms'; /* ----- SELECTORS ---- */ // filtered todo list const filteredTodoListState = selector({ key: 'filteredTodoListState', get: ({ get }) => { const filter = get(todoListFilterState); const list = get(todoListState); switch (filter) { case 'Show Completed': return list.filter((item) => item.isComplete); case 'Show Uncompleted': return list.filter((item) => !item.isComplete); default: return list; } }, }); // sorted todo list const sortedTodoListState = selector({ key: 'mismatchSortedTodoList', get: ({ get }) => { const sort = get(todoListSortState); const list = get(filteredTodoListState); const high = list.filter((item) => item.priority === 'high'); const medium = list.filter((item) => item.priority === 'medium'); const low = list.filter((item) => item.priority === 'low'); return sort === false ? list : [...high, ...medium, ...low]; }, }); // priority stats const todoListSortedStats = selector({ key: 'todoListSortedStats', get: ({ get }) => { const list = get(sortedTodoListState); return list.reduce((acc, cv) => { acc[cv.priority] = cv.priority in acc ? acc[cv.priority] + 1 : 1; return acc; }, {}); }, }); // completion (filter) stats const todoListStatsState = selector({ key: 'todoListStatsState', get: ({ get }) => { const list = get(todoListState); const totalNum = list.length; const totalCompletedNum = list.filter((todo) => todo.isComplete).length; const totalUncompletedNum = totalNum - totalCompletedNum; const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum; return { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted, }; }, }); // is filtered list non-empty? (determines whether check-all displays) const filteredListContentState = selector({ key: 'filteredListContentState', get: ({ get }) => !!get(filteredTodoListState).length, }); // WRITEABLE GET/SET SELECTOR - (un)check all filtered items const allCompleteState = selector({ key: 'mismatchAllComplete', // if any item in filteredList is not complete, allComplete is false get: ({ get }) => !get(filteredTodoListState).some(({ isComplete }) => !isComplete), set: ({ get, set }, newValue) => { // update ONLY items in filtered list const lookupTable = {}; get(todoListState).forEach((item) => { lookupTable[item.id] = item; }); get(filteredTodoListState).forEach((item) => { lookupTable[item.id] = { ...item, isComplete: newValue, }; }); set(todoListState, Object.values(lookupTable)); }, }); // WRITEABLE RESET SELECTOR - undo sort + filter const refreshFilterState = selector({ key: 'refreshFilterState', get: () => null, set: ({ reset }) => { reset(todoListSortState); reset(todoListFilterState); }, }); // PROMISE-BASED SELECTOR - fetch quote text const quoteTextState = selector({ key: 'quoteTextState', get: ({ get }) => { const quoteNumber = get(quoteNumberState); return fetch('https://type.fit/api/quotes') .then((response) => response.json()) .then((data) => { const quote = data[quoteNumber]; return `"${quote.text}"\n\t- ${quote.author || 'unknown'}`; }) .catch((err) => { console.error(err); return 'No quote available'; }); }, }); // ASYNC SELECTOR - fetch comic img // const xkcdState = selector({ // key: 'xkcdState', // get: async ({ get }) => { // const quoteNumber = get(quoteNumberState); // try { // // Fetch much be proxied through cors-anywhere to test on localhost // const response = await fetch( // `https://cors-anywhere.herokuapp.com/http://xkcd.com/${quoteNumber}/info.0.json`, // ); // const { img } = await response.json(); // return img; // } catch (err) { // // Fallback comic // return 'https://imgs.xkcd.com/comics/api.png'; // } // }, // }); const searchBarSelectorFam = selectorFamily({ key: 'searchBarSelectorFam', get: (searchFilter) => ({ get }) => get(searchResultState)[searchFilter], set: (searchFilter) => ({ get, set }, searchTerm) => { set(searchResultState, (prevState) => { const newResults = get(todoListState).filter((todo) => { if (searchTerm !== '' && todo.text.includes(searchTerm)) return searchFilter === 'all' ? true : todo.priority === searchFilter; return false; }); return { ...prevState, [searchFilter]: { searchTerm, results: newResults } }; }); }, }); export { filteredTodoListState, filteredListContentState, todoListStatsState, allCompleteState, sortedTodoListState, todoListSortedStats, refreshFilterState, quoteTextState, //xkcdState, searchBarSelectorFam, }; ================================================ FILE: demo-todo/src/styles/styles.css ================================================ /* -------------General Styles---------------- */ html { margin: 0; background-color: rgb(48, 48, 48); color: whitesmoke; font-family: 'Palanquin', sans-serif; overflow: hidden; } h1 { text-align: center; font-size: 2.3rem; color: #af6358; text-shadow: 1px 2px 2px rgba(250, 250, 250, 0.267); letter-spacing: 3px; font-style: italic; } input, button, select { background-color: rgb(63, 63, 63); border: 1px solid lightgray; border: none; padding: 20px; border-radius: 4px; color: whitesmoke; font-size: 16px; letter-spacing: 1px; } /* remove browser defaults */ button:focus { outline: none; } input:focus { outline: none; } /* ------------TodoList------------- */ /* topmost container */ .mainContainer { display: grid; height: 100vh; width: 100vw; grid-template-rows: 15fr 33fr 33fr; } /* overall row container for todo list display */ .todosDisplayRow { margin: 0 auto; width: 700px; } /* container for entire list display */ .todosContainer { background-color: rgb(63, 63, 63); box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); border-radius: 5px; padding: 5px 12px 0 12px; } /* -------------TodoItemCreator---------------- */ .itemCreator button { padding: 0px; } input::placeholder { font-style: italic; letter-spacing: 1.5px; } .itemCreator input { padding: 0 0 0 5%; width: 70%; height: 60px; } #radioContainer svg { margin-top: 11px; opacity: 0.7; } /* -------------TodoItem---------------- */ .itemContainer, .lowPriority, .mediumPriority, .highPriority { display: grid; grid-template-columns: 79fr 14fr 7fr; border-bottom: 1px solid rgba(245, 245, 245, 0.336); } /* dynamic checkbox color */ .highPriority svg { color: #ef5350; opacity: 0.7; margin-right: 28px; } .mediumPriority svg { color: #ffee58; opacity: 0.7; margin-right: 28px; } .lowPriority svg { color: #66bb6a; opacity: 0.7; margin-right: 28px; } #todoItem button { margin-left: 7px; } /* -------------TodoListFilter---------------- */ ul { display: grid; grid-template-columns: 27fr 27fr 27fr 6fr 6fr 6fr; margin: 0; padding: 0px; width: 100%; } .filter-button { margin-top: 10px; margin-bottom: 10px; border-right: 1px solid rgba(143, 143, 143, 0.26); padding: 10px 20px; border-radius: 0px 0px 4px 4px; } /* dynamic sort icon color */ #sortedWhite svg { color: whitesmoke; } #unsortedGray svg { color: rgba(245, 245, 245, 0.336); } #unsortedGray { width: 64px; } #statsSpan { display: grid; grid-template-columns: 30fr 30fr 30fr; align-items: center; } #highSpan { color: #ef5350; opacity: 0.7; margin-right: 4px; } #mediumSpan { color: #ffee58; opacity: 0.7; margin-right: 4px; } #lowSpan { color: #66bb6a; opacity: 0.7; } /* filter stats (number) */ button span { opacity: 0.6; } /* ----------QuoteBox---------- */ #quoteContainer { display: flex; flex-direction: column; justify-content: space-between; } #quoteContainer p { white-space: pre-wrap; } .quoteBox { display: flex; flex-direction: row; justify-content: space-between; background-color: rgb(63, 63, 63); box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); border-radius: 5px; margin: auto; padding: 12px; } .quoteBox img { margin: 20px; height: 150px; width: 150px; } .quoteBox button { width: 150px; } .quoteBox button:hover { border: 1px solid whitesmoke; cursor: pointer; } /* ---------- TodoQuickCheck ---------- */ #quickCheck { font-family: Arial, Helvetica, sans-serif; } #quickCheck svg { color: #af6358; } /* ---------- Search ---------- */ .searchContainer { margin-top: 100px; background-color: rgb(63, 63, 63); box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); border-radius: 5px; padding: 5px 12px 12px 12px; display: grid; grid-template-columns: 70% 30%; } .searchField { border-bottom: solid 1px whitesmoke; border-radius: 0; } .prioritySelect { grid-column-start: 2; border-bottom: solid 1px whitesmoke; border-radius: 0; } .searchResults { grid-column-start: span 3; } ================================================ FILE: demo-todo/webpack.config.js ================================================ const path = require('path'); module.exports = { entry: path.resolve(__dirname, './src/index.js'), output: { filename: 'bundle.js', }, devServer: { contentBase: path.resolve(__dirname, './src'), historyApiFallback: true, }, mode: process.env.NODE_ENV, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/transform-runtime'], }, }, { test: /\.css$/, use: [ { loader: 'style-loader', }, { loader: 'css-loader', }, ], }, ], }, resolve: { extensions: ['.js', '.jsx'], }, }; ================================================ FILE: demo-zustand-todo/.babelrc ================================================ { "presets": [ ["@babel/preset-env"], "@babel/preset-react" ], "plugins": [ //"react-hot-loader/babel" ] } ================================================ FILE: demo-zustand-todo/LICENSE ================================================ MIT License Copyright (c) 2020 Michelle Holland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: demo-zustand-todo/__tests__/sampleTest.js ================================================ import { renderHook, act } from '@testing-library/react'; import useStore from '../src/store/store'; describe('INITIAL RENDER', () => { const { result } = renderHook(useStore); it('todoListState should initialize correctly', () => { expect(result.current.todoListState).toStrictEqual([]); }); it('todoListFilterState should initialize correctly', () => { expect(result.current.todoListFilterState).toStrictEqual('Show All'); }); it('todoListSortState should initialize correctly', () => { expect(result.current.todoListSortState).toStrictEqual(false); }); it('quoteText should initialize correctly', () => { expect(result.current.quoteText).toStrictEqual(''); }); it('quoteNumber should initialize correctly', () => { expect(result.current.quoteNumber).toStrictEqual(0); }); it('checkBox should initialize correctly', () => { expect(result.current.checkBox).toStrictEqual(false); }); it('searchResultState should initialize correctly', () => { expect(result.current.searchResultState).toStrictEqual({ all: { searchTerm: '', results: [] }, high: { searchTerm: '', results: [] }, medium: { searchTerm: '', results: [] }, low: { searchTerm: '', results: [] }, }); }); }); describe('STATE CHANGES', () => { const { result } = renderHook(useStore); it('checkBox & quoteText & todoListState should update correctly', () => { const { result } = renderHook(useStore); act(() => { result.current.setCheckBox(); result.current.setCheckBox(); result.current.changeQuoteText( '"Your ability to learn faster than your competition is your only sustainable competitive advantage."\n\t- Arie de Gues', ); result.current.addTodoListItem({ id: 2, text: 'tennis', priority: 'low', isComplete: false }); }); expect(result.current.checkBox).toStrictEqual(true); expect(result.current.quoteText).toStrictEqual( '"Your ability to learn faster than your competition is your only sustainable competitive advantage."\n\t- Arie de Gues', ); expect(result.current.todoListState).toStrictEqual([ { id: 2, text: 'tennis', priority: 'low', isComplete: false }, ]); }); it('checkBox & todoListState should update correctly', () => { const { result } = renderHook(useStore); act(() => { result.current.setCheckBox(); result.current.setCheckBox(); result.current.addTodoListItem({ id: 3, text: 'hockey', priority: 'low', isComplete: false }); result.current.setCheckBox(); }); expect(result.current.checkBox).toStrictEqual(false); expect(result.current.todoListState).toStrictEqual([ { id: 2, text: 'tennis', priority: 'low', isComplete: false }, { id: 3, text: 'hockey', priority: 'low', isComplete: false }, ]); }); it('todoListState should update correctly', () => { const { result } = renderHook(useStore); act(() => { result.current.addTodoListItem({ id: 4, text: 'hocka', priority: 'low', isComplete: false }); result.current.setCheckBox(); }); expect(result.current.todoListState).toStrictEqual([ { id: 2, text: 'tennis', priority: 'low', isComplete: false }, { id: 3, text: 'hockey', priority: 'low', isComplete: false }, { id: 4, text: 'hocka', priority: 'low', isComplete: false }, ]); }); it('todoListState & searchResultState should update correctly', () => { const { result } = renderHook(useStore); act(() => { result.current.addTodoListItem({ id: 5, text: 'canoe', priority: 'low', isComplete: false }); result.current.setCheckBox(); result.current.setSearchState('c', 'all'); }); expect(result.current.todoListState).toStrictEqual([ { id: 2, text: 'tennis', priority: 'low', isComplete: false }, { id: 3, text: 'hockey', priority: 'low', isComplete: false }, { id: 4, text: 'hocka', priority: 'low', isComplete: false }, { id: 5, text: 'canoe', priority: 'low', isComplete: false }, ]); expect(result.current.searchResultState).toStrictEqual({ all: { searchTerm: 'c', results: [ { id: 3, text: 'hockey', priority: 'low', isComplete: false }, { id: 4, text: 'hocka', priority: 'low', isComplete: false }, { id: 5, text: 'canoe', priority: 'low', isComplete: false }, ], }, high: { searchTerm: '', results: [] }, medium: { searchTerm: '', results: [] }, low: { searchTerm: '', results: [] }, }); }); it('searchResultState should update correctly', () => { const { result } = renderHook(useStore); act(() => { result.current.setSearchState('ca', 'all'); }); expect(result.current.searchResultState).toStrictEqual({ all: { searchTerm: 'ca', results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], }, high: { searchTerm: '', results: [] }, medium: { searchTerm: '', results: [] }, low: { searchTerm: '', results: [] }, }); }); it('searchResultState should update correctly', () => { const { result } = renderHook(useStore); act(() => { result.current.setSearchState('can', 'all'); }); expect(result.current.searchResultState).toStrictEqual({ all: { searchTerm: 'can', results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], }, high: { searchTerm: '', results: [] }, medium: { searchTerm: '', results: [] }, low: { searchTerm: '', results: [] }, }); }); it('searchResultState should update correctly', () => { const { result } = renderHook(useStore); act(() => { result.current.setSearchState('cano', 'all'); }); expect(result.current.searchResultState).toStrictEqual({ all: { searchTerm: 'cano', results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], }, high: { searchTerm: '', results: [] }, medium: { searchTerm: '', results: [] }, low: { searchTerm: '', results: [] }, }); }); }); ================================================ FILE: demo-zustand-todo/package.json ================================================ { "name": "chromogen-todo", "version": "1.0.2", "description": "demo todo app for Chromogen using React + Recoil", "main": "index.js", "scripts": { "start": "webpack-dev-server --open", "test": "jest --verbose", "update": " npm run uninstall && npm run install && npm run start", "uninstall": "npm uninstall chromogen", "install": "npm install ../package", "buildPackage": "tsc", "tarballUpdate": "npm --prefix ../package run build && npm pack ../package && npm uninstall chromogen && npm install ./chromogen-5.0.1.tgz && npm start" }, "keywords": [ "react", "recoil", "chromogen", "demo", "example", "todo" ], "repository": { "type": "git", "url": "https://github.com/open-source-labs/Chromogen.git" }, "contributors": [ { "name": "Brach Burdick", "url": "https://github.com/sirbrachthepale/" }, { "name": "Francois Denavaut", "url": "https://github.com/dnvt/" }, { "name": "Maggie Kwan", "url": "https://github.com/maggiekwan/" }, { "name": "Lawrence Liang", "url": "https://github.com/Lawliang/" }, { "name": "Michelle Holland", "url": "https://github.com/michellebholland/" }, { "name": "Jim Chen", "url": "https://github.com/chenchingk" }, { "name": "Andy Wang", "url": "https://github.com/andywang23" }, { "name": "Connor Rose Delisle", "url": "https://github.com/connorrose" } ], "license": "MIT", "devDependencies": { "@babel/core": "^7.11.1", "@babel/plugin-transform-runtime": "^7.11.0", "@babel/preset-env": "^7.11.0", "@babel/preset-react": "^7.10.4", "@testing-library/react": "^13.1.1", "babel-loader": "^8.1.0", "css-loader": "^4.2.1", "identity-obj-proxy": "^3.0.0", "jest": "^26.4.2", "prettier": "^2.7.1", "style-loader": "^1.2.1", "webpack": "^4.44.1", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" }, "peerDependencies": { "typescript": "^4.0.3" }, "dependencies": { "@babel/runtime": "^7.11.2", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.4", "@mui/icons-material": "^5.10.6", "@mui/material": "^5.10.6", "babel-jest": "^26.3.0", "chromogen": "file:chromogen-4.0.4.tgz", "file-loader": "^6.2.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-test-renderer": "^18.1.0", "recoil": "0.7.5", "typescript": "^4.0.3", "url-loader": "^4.1.1", "zustand": "^4.1.1" }, "jest": { "moduleNameMapper": { "\\.(css|less)$": "identity-obj-proxy" } } } ================================================ FILE: demo-zustand-todo/src/components/App.jsx ================================================ import React from 'react'; import { ChromogenZustandObserver } from 'chromogen'; import TodoList from './TodoList'; import '../styles/styles.css'; const App = () => ( <> ); export default App; ================================================ FILE: demo-zustand-todo/src/components/Quotes.jsx ================================================ import React from 'react'; import shallow from 'zustand/shallow'; import useToDoStore from '../store/store'; import { useEffect } from 'react'; const selector = (state) => ({ changeQuoteText: state.changeQuoteText, quoteText: state.quoteText, }); const Quotes = () => { const { changeQuoteText, quoteText } = useToDoStore(selector, shallow); const fetchMe = () => { let randomNum = Math.floor(Math.random() * 1643); fetch('https://type.fit/api/quotes') .then((response) => response.json()) .then((data) => { const quote = data[randomNum]; changeQuoteText(`"${quote.text}"\n\t- ${quote.author || 'unknown'}`); }) .catch((err) => { console.error(err); return 'No quote available'; }); }; useEffect(() => fetchMe(), []); return ( <> ); }; export default Quotes; ================================================ FILE: demo-zustand-todo/src/components/ReadOnlyTodoItem.jsx ================================================ import React from 'react'; import Checkbox from '@mui/material/Checkbox'; import '../styles/styles.css'; import useToDoStore from '../store/store'; const ReadOnlyTodoItem = ({ item }) => { const checkBoxClasses = { low: 'lowPriority', medium: 'mediumPriority', high: 'highPriority', }; const todoList = useToDoStore((state) => state.todoListState); return todoList.find((todo) => todo.id === item.id) ? (
todo.id === item.id).isComplete} color="default" inputProps={{ 'aria-label': 'primary checkbox' }} style={{ cursor: 'default' }} />
) : null; }; export default ReadOnlyTodoItem; ================================================ FILE: demo-zustand-todo/src/components/SearchBar.jsx ================================================ import React, { useState } from 'react'; import useToDoStore from '../store/store'; import shallow from 'zustand/shallow'; import ReadOnlyTodoItem from './ReadOnlyTodoItem'; const selector = (state) => ({ searchResultState: state.searchResultState, setSearchState: state.setSearchState, }); const SearchBar = () => { const [searchFilter, setSearchFilter] = useState('all'); const [searchText, setSearchText] = useState(''); const { searchResultState, setSearchState } = useToDoStore(selector, shallow); const searchResults = searchResultState[searchFilter]; const onSearchTextChange = (e) => { setSearchText(e.target.value); setSearchState(e.target.value, searchFilter); }; const onSelectChange = (e) => { setSearchText(''); setSearchFilter(e.target.value); }; return (
{searchResults.results.map((result, idx) => ( ))}
); }; export default SearchBar; ================================================ FILE: demo-zustand-todo/src/components/TodoItem.jsx ================================================ import React from 'react'; import Checkbox from '@mui/material/Checkbox'; import '../styles/styles.css'; import shallow from 'zustand/shallow'; import useToDoStore from '../store/store'; const selector = (state) => ({ todoListState: state.todoListState, deleteTodoListItem: state.deleteTodoListItem, editItemText: state.editItemText, toggleItemCompletion: state.toggleItemCompletion, }); const TodoItem = ({ item }) => { const { deleteTodoListItem, editItemText, toggleItemCompletion } = useToDoStore( selector, shallow, ); const checkBoxClasses = { low: 'lowPriority', medium: 'mediumPriority', high: 'highPriority', }; return (
editItemText(e.target.value, item.id)} /> toggleItemCompletion(item.id)} />
); }; export default TodoItem; ================================================ FILE: demo-zustand-todo/src/components/TodoItemCreator.jsx ================================================ /* eslint-disable react/jsx-props-no-spreading */ import React, { useState } from 'react'; import RadioGroup from '@mui/material/RadioGroup'; import FormControlLabel from '@mui/material/FormControlLabel'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; import Radio from '@mui/material/Radio'; import useToDoStore from '../store/store'; const selector = (state) => state.addTodoListItem; // utility for creating unique Id let id = 1; const getId = () => { id += 1; return id; }; const TodoItemCreator = () => { const [inputValue, setInputValue] = useState(''); const [priorityValue, setPriorityValue] = useState('low'); const addTodoListItem = useToDoStore(selector); const addItem = () => { addTodoListItem({ id: getId(), text: inputValue, priority: priorityValue, isComplete: false, }); setInputValue(''); setPriorityValue('low'); }; const onChange = ({ target: { value } }) => { setInputValue(value); }; const handleChange = (event) => { setPriorityValue(event.target.value); }; /* MUI Radio Button styles */ const GreenRadio = (props) => ; const YellowRadio = (props) => ; const RedRadio = (props) => ; return (
} value="high" /> } value="medium" /> } value="low" />
); }; export default TodoItemCreator; ================================================ FILE: demo-zustand-todo/src/components/TodoList.jsx ================================================ import React from 'react'; import TodoItem from './TodoItem'; import TodoItemCreator from './TodoItemCreator'; import TodoListFilters from './TodoListFilters'; import TodoQuickCheck from './TodoQuickCheck'; import Quotes from './Quotes'; import SearchBar from './SearchBar'; import '../styles/styles.css'; import shallow from 'zustand/shallow'; import useToDoStore from '../store/store'; const selector = (state) => ({ todoListState: state.todoListState, todoListFilterState: state.todoListFilterState, todoListSortState: state.todoListSortState, }); const filterList = (list, filter) => { switch (filter) { case 'Show Completed': return list.filter((item) => item.isComplete); case 'Show Uncompleted': return list.filter((item) => !item.isComplete); default: return list; } }; const sortList = (list, sortingMethod) => { if (!sortingMethod) return list; const high = list.filter((item) => item.priority === 'high'); const medium = list.filter((item) => item.priority === 'medium'); const low = list.filter((item) => item.priority === 'low'); return [...high, ...medium, ...low]; }; const TodoList = () => { const { todoListState, todoListFilterState, todoListSortState } = useToDoStore(selector, shallow); const todoList = sortList(filterList(todoListState, todoListFilterState), todoListSortState); return (
Loading...}>

To-Do List

{todoList.map((todoItem) => ( ))}
); }; export default TodoList; ================================================ FILE: demo-zustand-todo/src/components/TodoListFilters.jsx ================================================ import React, { useState } from 'react'; import SortIcon from '@mui/icons-material/Sort'; import EqualizerIcon from '@mui/icons-material/Equalizer'; import RefreshIcon from '@mui/icons-material/Refresh'; import useToDoStore from '../store/store'; import shallow from 'zustand/shallow'; const selector = (state) => ({ todoListFilterState: state.todoListFilterState, todoListState: state.todoListState, resetFiltersAndSorted: state.resetFiltersAndSorted, todoListSortState: state.todoListSortState, toggleSort: state.toggleSort, setFilter: state.setFilter, }); const TodoListFilters = () => { const { todoListFilterState, todoListState, resetFiltersAndSorted, todoListSortState, toggleSort, setFilter, } = useToDoStore(selector, shallow); // // selector - grabs totals for each category const { high, medium, low } = todoListState.reduce((acc, cur) => { acc[cur.priority] = (acc[cur.priority] ?? 0) + 1; return acc; }, {}); // // toggle priority stats display const [displayStats, setDisplayStats] = useState(false); // // selector - totals for each filter const totalNum = todoListState.length; const totalCompletedNum = todoListState.filter((todo) => todo.isComplete).length; const totalUncompletedNum = todoListState.filter((todo) => !todo.isComplete).length; const updateFilter = ({ target: { value } }) => setFilter(value); const toggleDisplayStats = () => setDisplayStats(!displayStats); const reset = () => { setDisplayStats(false); // displayStats is local state resetFiltersAndSorted(); }; const sortIconColor = { true: 'sortedWhite', false: 'unsortedGray', }; return (
); }; export default TodoListFilters; ================================================ FILE: demo-zustand-todo/src/components/TodoQuickCheck.jsx ================================================ import React from 'react'; import Checkbox from '@mui/material/Checkbox'; import shallow from 'zustand/shallow'; import useToDoStore from '../store/store'; import { useEffect } from 'react'; const selector = (state) => ({ setAllComplete: state.setAllComplete, checkBox: state.checkBox, setCheckBox: state.setCheckBox, }); const TodoQuickCheck = () => { const { setAllComplete, checkBox, setCheckBox } = useToDoStore(selector, shallow); useEffect(() => setCheckBox()); return (
setAllComplete()} /> All
); }; export default TodoQuickCheck; ================================================ FILE: demo-zustand-todo/src/index.html ================================================ Chromogen Zustand Demo To-Do
================================================ FILE: demo-zustand-todo/src/index.js ================================================ /* eslint-disable react/jsx-filename-extension */ import React from 'react'; import App from './components/App'; import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('app')); root.render(); ================================================ FILE: demo-zustand-todo/src/store/store.js ================================================ import { chromogenZustandMiddleware } from 'chromogen'; import { create } from 'zustand'; const useToDoStore = create( chromogenZustandMiddleware((set) => ({ todoListState: [], todoListFilterState: 'Show All', todoListSortState: false, resetFiltersAndSorted: () => set( () => ({ todoListFilterState: 'Show All', todoListSortState: false }), false, 'resetFiltersAndSorted', ), toggleSort: () => set((state) => ({ todoListSortState: !state.todoListSortState }), false, 'toggleSort'), setFilter: (filter) => set(() => ({ todoListFilterState: filter }), false, 'setFilter'), quoteText: '', changeQuoteText: (text) => set(() => ({ quoteText: text }), false, 'changeQuoteText', text), quoteNumber: 0, changeQuoteNumber: () => set(() => ({ quoteNumber: Math.floor(Math.random() * 1643) }), false, 'changeQuoteNumber'), setAllComplete: () => set( (state) => ({ todoListState: state.todoListState.some((todo) => todo.isComplete === false) ? state.todoListState.map((todo) => { return { ...todo, isComplete: true }; }) : state.todoListState.map((todo) => { return { ...todo, isComplete: false }; }), }), false, 'setAllComplete', ), checkBox: false, setCheckBox: () => set( (state) => ({ checkBox: state.todoListState.some((todo) => todo.isComplete === false) ? false : true, }), false, 'setCheckBox', ), addTodoListItem: (todo) => set( (state) => ({ todoListState: [...state.todoListState, todo] }), false, 'addTodoListItem', todo, ), deleteTodoListItem: (id) => set( (state) => ({ todoListState: state.todoListState.filter((todo) => todo.id !== id) }), false, 'deleteTodoListItem', id, ), editItemText: (text, id) => set( (state) => ({ todoListState: state.todoListState.map((todo) => { if (todo.id === id) { return { ...todo, text: text }; } else { return todo; } }), }), false, 'editItemText', text, id, ), toggleItemCompletion: (id) => set( (state) => ({ todoListState: state.todoListState.map((todo) => { if (todo.id === id) { return { ...todo, isComplete: !todo.isComplete }; } else { return todo; } }), }), false, 'toggleItemCompletion', id, ), searchResultState: { all: { searchTerm: '', results: [], }, high: { searchTerm: '', results: [], }, medium: { searchTerm: '', results: [], }, low: { searchTerm: '', results: [], }, }, setSearchState: (searchTerm, priority) => set( (state) => { if (searchTerm === '') return { searchResultState: { ...state.searchResultState, [priority]: { searchTerm, results: [] }, }, }; let results = [...state.todoListState].filter((todo) => todo.text.includes(searchTerm)); if (priority !== 'all') results = results.filter((todo) => todo.priority === priority); return { searchResultState: { ...state.searchResultState, [priority]: { searchTerm, results } }, }; }, false, 'setSearchState', searchTerm, priority, ), })), ); export default useToDoStore; ================================================ FILE: demo-zustand-todo/src/styles/styles.css ================================================ *, *::before, *::after { box-sizing: border-box; } * { margin: 0; } html, body, #root, /* for create-react-app */ #__next /* for Next.js */ { height: 100%; } body { font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; } img, picture, video, canvas, svg { display: block; max-width: 100%; } input, button, textarea, select { font: inherit; } p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; } #root, #__next { isolation: isolate; } /* -------------General Styles---------------- */ html { margin: 0; background-color: #2e3237; color: #9ee6f7; overflow-x: hidden; } h1 { text-align: center; font-size: 2.3rem; color: #9ee6f7; text-shadow: 1px 2px 2px rgba(250, 250, 250, 0.267); letter-spacing: 3px; } input, button, select { background-color: transparent; /* border: 1px solid lightgray; */ border: none; padding: 20px 0; border-radius: 4px; color: #9ee6f7; font-size: 18px; letter-spacing: 1px; } /* remove browser defaults */ button:focus { outline: none; } input:focus { outline: none; } /* ------------TodoList------------- */ /* topmost container */ .mainContainer { display: flex; flex-direction: column; height: 100vh; width: 100vw; overflow-y: scroll; } .wrapper { display: flex; margin: 0 auto; padding-inline: 32px; width: 100%; max-width: 800px; height: 100%; flex-direction: column; padding-bottom: 24px; } /* overall row container for todo list display */ .todosDisplayRow { margin: 0 auto; width: 100%; height: 100%; color: #9ee6f7; } a { color: #9ee6f7; text-decoration: none; } a:hover { text-decoration: underline; } .todosDisplayRow h1 { padding-block: 40px; } /* container for entire list display */ .todosContainer { background-color: rgb(255, 255, 255, 0.1); box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); border-radius: 5px; padding: 5px 12px 0 12px; } /* -------------TodoItemCreator---------------- */ .itemCreator { display: flex; border-bottom: 1px solid rgba(245, 245, 245, 0.336); } .itemCreator button { padding: 0 24px; } input::placeholder { letter-spacing: 1.5px; } .itemCreator input { padding: 0 0 0 12px; flex-grow: 1; height: 60px; } #radioContainer svg { margin-top: 11px; opacity: 0.7; } label.MuiFormControlLabel-root { margin-left: 0; margin-right: 0; } /* -------------TodoItem---------------- */ .itemContainer, .lowPriority, .mediumPriority, .highPriority { display: grid; grid-template-columns: 79fr 14fr 7fr; border-bottom: 1px solid rgba(245, 245, 245, 0.336); } /* dynamic checkbox color */ .highPriority svg { color: #f10101; opacity: 0.7; margin-right: 28px; } .mediumPriority svg { color: #ffe600; opacity: 0.7; margin-right: 28px; } .lowPriority svg { color: #05fb11; opacity: 0.7; margin-right: 28px; } #todoItem button { margin-left: 7px; } /* -------------TodoListFilter---------------- */ ul { display: grid; grid-template-columns: 27fr 27fr 27fr 6fr 6fr 6fr; margin: 0; padding: 0px; width: 100%; } .filter-button { margin-top: 10px; margin-bottom: 10px; border-right: 1px solid rgba(234, 230, 230, 0.26); padding: 10px 20px; border-radius: 0px 0px 4px 4px; } /* dynamic sort icon color */ #sortedWhite svg { color: whitesmoke; } #unsortedGray svg { color: rgba(245, 245, 245, 0.336); } #unsortedGray { width: 64px; } #statsSpan { display: grid; grid-template-columns: 30fr 30fr 30fr; align-items: center; } #highSpan { color: #ef5350; opacity: 0.7; margin-right: 4px; } #mediumSpan { color: #ffee58; opacity: 0.7; margin-right: 4px; } #lowSpan { color: #66bb6a; opacity: 0.7; } /* filter stats (number) */ button span { opacity: 0.6; } /* ----------QuoteBox---------- */ #quoteContainer { display: flex; flex-direction: column; width: 100%; /* justify-content: space-between; */ font-size: 18px; } #quoteContainer p { white-space: pre-wrap; padding-bottom: 24px; } .quoteBox { display: flex; flex-direction: row; justify-content: space-between; border: 1px solid rgba(234, 230, 230, 0.1); /* background-color: rgb(63, 63, 63); */ /* box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); */ border-radius: 5px; padding: 16px 24px; } .quoteBox img { margin: 20px; height: 150px; width: 150px; } .quoteBox button { width: 150px; padding: 24 0; text-align: left; } .quoteBox button:hover { cursor: pointer; /* background-color: rgba(255, 255, 255, 0.1); */ } /* .quoteBox button:active { background-color: #75acb990; } */ /* ---------- TodoQuickCheck ---------- */ /* #quickCheck { } */ #quickCheck svg { color: #9ee6f7; } button { cursor: pointer; } /* ---------- Search ---------- */ .searchContainer { display: sticky; margin-top: 100px; background-color: rgb(255, 255, 255, 0.1); box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); border-radius: 5px; padding: 5px 12px 12px 12px; display: grid; grid-template-columns: 70% 30%; } .searchField { border-bottom: solid 1px whitesmoke; border-radius: 0; padding-left: 12px; } .prioritySelect { grid-column-start: 2; border-bottom: solid 1px whitesmoke; border-radius: 0; -webkit-appearance-select: none; } .searchResults { grid-column-start: span 3; } #newChromogenLogo { display: flex; justify-content: center; width: 400px; height: 300px; } select { -webkit-appearance-select: none; /* background: url("data:image/svg+xml;utf8,") no-repeat; */ } /* */ .w-tc-editor[data-color-mode*='dark'], [data-color-mode*='dark'] .w-tc-editor, [data-color-mode*='dark'] .w-tc-editor-var, body[data-color-mode*='dark'] { --color-fg-default: #ddd; --color-canvas-subtle: #161b22; --color-prettylights-syntax-comment: #818c97; --color-prettylights-syntax-entity-tag: #ed81b0; --color-prettylights-syntax-entity: #d2a8ff; --color-prettylights-syntax-sublimelinter-gutter-mark: #ddd; --color-prettylights-syntax-constant: #ed8876; --color-prettylights-syntax-string: #68afc8; --color-prettylights-syntax-keyword: #ed81b0; --color-prettylights-syntax-markup-bold: #c9d1d9; } ================================================ FILE: demo-zustand-todo/webpack.config.js ================================================ const path = require('path'); module.exports = { entry: path.resolve(__dirname, './src/index.js'), output: { filename: 'bundle.js', }, devServer: { contentBase: path.resolve(__dirname, './src'), historyApiFallback: true, }, mode: process.env.NODE_ENV, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/transform-runtime'], }, }, { test: /\.css$/, use: [ { loader: 'style-loader', }, { loader: 'css-loader', }, ], }, { test: /\.(png|jpg)$/, use: ['file-loader', 'url-loader?limit=8192'], }, ], }, resolve: { extensions: ['.js', '.jsx'], }, }; ================================================ FILE: jenkins/Jenkinsfile ================================================ pipeline { agent { docker { image 'node:lts-buster-slim' args '-p 3003:3003' } } environment { CI = 'true' } stages { stage('Build') { steps { sh 'npm --prefix ./package install' } } stage('Test') { steps { sh './jenkins/scripts/test.sh' } } stage('Deliver') { steps { sh './jenkins/scripts/deliver.sh' input message: 'Finished using the web site? (Click "Proceed" to continue)' sh './jenkins/scripts/kill.sh' } } } } ================================================ FILE: jenkins/scripts/deliver.sh ================================================ #!/usr/bin/env sh echo 'The following "npm" command builds your Node.js/React application for' echo 'production in the local "build" directory (i.e. within the' echo '"/var/jenkins_home/workspace/simple-node-js-react-app" directory),' echo 'correctly bundles React in production mode and optimizes the build for' echo 'the best performance.' set -x npm run build set +x echo 'The following "npm" command runs your Node.js/React application in' echo 'development mode and makes the application available for web browsing.' echo 'The "npm start" command has a trailing ampersand so that the command runs' echo 'as a background process (i.e. asynchronously). Otherwise, this command' echo 'can pause running builds of CI/CD applications indefinitely. "npm start"' echo 'is followed by another command that retrieves the process ID (PID) value' echo 'of the previously run process (i.e. "npm start") and writes this value to' echo 'the file ".pidfile".' set -x npm --prefix ./package run symlink & sleep 1 echo $! > .pidfile set +x echo 'Now...' echo 'Visit http://localhost:3003 to see your Node.js/React application in action.' echo '(This is why you specified the "args ''-p 3003:3003''" parameter when you' echo 'created your initial Pipeline as a Jenkinsfile..)' ================================================ FILE: jenkins/scripts/kill.sh ================================================ #!/usr/bin/env sh echo 'The following command terminates the "npm start" process using its PID' echo '(written to ".pidfile"), all of which were conducted when "deliver.sh"' echo 'was executed.' set -x kill $(cat .pidfile) ================================================ FILE: jenkins/scripts/test.sh ================================================ #!/usr/bin/env sh echo 'The following "npm" command (if executed) installs the "cross-env"' echo 'dependency into the local "node_modules" directory, which will ultimately' echo 'be stored in the Jenkins home directory. As described in' echo 'https://docs.npmjs.com/cli/install, the "--save-dev" flag causes the' echo '"cross-env" dependency to be installed as "devDependencies". For the' echo 'purposes of this tutorial, this flag is not important. However, when' echo 'installing this dependency, it would typically be done so using this' echo 'flag. For a comprehensive explanation about "devDependencies", see' echo 'https://stackoverflow.com/questions/18875674/whats-the-difference-between-dependencies-devdependencies-and-peerdependencies.' set -x npm install --save-dev cross-env set +x echo 'The following "npm" command tests that your simple Node.js/React' echo 'application renders satisfactorily. This command actually invokes the test' echo 'runner Jest (https://facebook.github.io/jest/).' set -x npm --prefix ./package run test ================================================ FILE: package/LICENSE ================================================ MIT License Copyright (c) 2020 OSLabs Beta Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: package/README.md ================================================

Chromogen

chromogen logo

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


[![npm version](https://img.shields.io/npm/v/chromogen)](https://www.npmjs.com/package/chromogen) [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/open-source-labs/Chromogen/blob/master/LICENSE)

**Now Compatible with React V18** 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.


## Installation for Zustand Apps Before using Chromogen, you'll need to make two changes to your application: 1. Import the `` component and render it alongside any other components in `` 2. Import `chromogenZustandMiddleware` function from Chromogen. This will be used as middleware when setting up your store. ### Import the ChromogenZustandObserver component Import `ChromogenZustandObserver`. ChromogenZustandObserver can be rendered alongside any other components in ``. ```jsx import React from 'react'; import { ChromogenZustandObserver } from 'chromogen'; import TodoList from './TodoList'; const App = () => ( <> ); export default App; ``` 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. 1. _Overwrite State_ (boolean) - Without middleware, this defaults to `false`, but you'll need to explicitly provide a value when using Chromogen. 2. _Action Name_ - Used for test generation 3. _Action Parameters_ - If the action requires input parameters, pass these in after the Action Name. ```jsx import { chromogenZustandMiddleware } from 'chromogen'; import create from 'zustand'; const useStore = create( chromogenZustandMiddleware((set) => ({ counter: 0, color: 'black', prioritizeTask: ['walking', 5], addCounter: () => set(() => ({ counter: (counter += 1) }), false, 'addCounter'), changeColor: (newColor) => set(() => ({ color: newColor }), false, 'changeColor', newColor), setTaskPriority: (task, priority) => set(() => ({ prioritizeTask: [task, priority] }), false, 'setTaskPriority', task, priority), })), ); export default useStore; ```

## Installation for Recoil Apps Before running Chromogen, you'll need to make two changes to your application: 1. Import the `` component as a child of `` 1. Import the `atom` and `selector` functions from Chromogen instead of Recoil Note: These changes do have a small performance cost, so they should be reverted before deploying to production.
### Import the ChromogenObserver component 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. ```jsx import React from 'react'; import { RecoilRoot } from 'recoil'; import { ChromogenObserver } from 'chromogen'; import MyComponent from './components/MyComponent.jsx'; const App = (props) => ( ); export default App; ``` 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: ```jsx import * as store from './store'; // ... ; ``` If your store utilizes seprate files for various pieces of state, you can pass all of the imports in an array: ```jsx import * as atoms from './store/atoms'; import * as selectors from './store/selectors'; import * as misc from './store/arbitraryRecoilState'; // ... ; ```
### Import atom & selector functions from Chromogen 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. ```js import { atom, selector } from 'chromogen'; export const fooState = atom({ key: 'fooState', default: {}, }); export const barState = selector({ key: 'barState', get: ({ get }) => { const derivedState = get(fooState); return derivedState.baz || 'value does not exist'; }, }); ```

## Usage for All Apps After following the installation steps above, launch your application as normal. You should see two buttons in the bottom left corner.
![Buttons](https://github.com/open-source-labs/Chromogen/raw/master/assets/README-root/ultratrimmedDemo.gif)
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. 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. 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. 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** 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`. The current tests check whether state has changed after an interaction and checks whether the resulting state change variables have been updated as expected.

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. ================================================ FILE: package/babel.config.js ================================================ module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-react', '@babel/preset-typescript', ], }; ================================================ FILE: package/index.ts ================================================ /* eslint-disable */ import { atom, selector, atomFamily, selectorFamily } from './recoil_generator/src/api/api'; import { ChromogenZustandObserver } from './zustand_generator/src/component/ChromogenZustandObserver'; import { ChromogenObserver } from './recoil_generator/src/component/ChromogenObserver'; import { chromogenZustandMiddleware } from './zustand_generator/src/api/api'; import Editor from './zustand_generator/src/component/Editor'; // CHROMGOEN FAMILY APIs ARE CURRENTLY UNSTABLE export { atom, selector, atomFamily, selectorFamily, ChromogenObserver, chromogenZustandMiddleware, ChromogenZustandObserver, Editor, }; ================================================ FILE: package/package.json ================================================ { "name": "chromogen", "version": "5.0.1", "description": "simple, interaction-driven Jest test generator for Recoil and React Hooks apps", "main": "build/index.js", "keywords": [ "react", "recoil", "jest", "testing" ], "files": [ "build" ], "scripts": { "prepublishOnly": "npm run build", "build": "tsc", "test": "jest --verbose --coverage", "localUpdate": "tsc && npm --prefix ../demo-zustand-todo run update", "tarballUpdate": "npm --prefix ../demo-zustand-todo run symlink", "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" }, "repository": { "type": "git", "url": "https://github.com/open-source-labs/Chromogen.git" }, "contributors": [ { "name": "Brach Burdick", "url": "https://github.com/sirbrachthepale/" }, { "name": "Francois Denavaut", "url": "https://github.com/dnvt/" }, { "name": "Maggie Kwan", "url": "https://github.com/maggiekwan/" }, { "name": "Lawrence Liang", "url": "https://github.com/Lawliang/" }, { "name": "Michelle Holland", "url": "https://github.com/michellebholland/" }, { "name": "Jim Chen", "url": "https://github.com/chenchingk" }, { "name": "Andy Wang", "url": "https://github.com/andywang23" }, { "name": "Connor Rose Delisle", "url": "https://github.com/connorrose" }, { "name": "Amy Yee", "url": "https://github.com/amyy98" }, { "name": "Cameron Greer", "url": "https://github.com/cgreer011" }, { "name": "Jinseon Shin", "url": "https://github.com/wlstjs" }, { "name": "Nicholas Shay", "url": "https://github.com/nicholasjs" }, { "name": "Ryan Tumel", "url": "https://github.com/rtumel123" }, { "name": "Marcellies Pettiford", "url": "https://github.com/mp-04" }, { "name": "Sung Kim", "url": "https://github.com/smk53664" }, { "name": "Lina Lee", "url": "https://github.com/lina4lee" }, { "name": "Erica Oh", "url": "https://github.com/ericaysoh" }, { "name": "Dani Almaraz", "url": "https://github.com/dtalmaraz" }, { "name": "Craig Boswell", "url": "https://github.com/crgb0s" }, { "name": "Hussein Ahmed", "url": "https://github.com/Hali3030" }, { "name": "Ian Kila", "url": "https://github.com/iannkila" }, { "name": "Yuehao Wong", "url": "https://github.com/yuehaowong" } ], "license": "MIT", "bugs": { "url": "https://github.com/open-source-labs/Chromogen/issues" }, "homepage": "https://github.com/open-source-labs/Chromogen#readme", "peerDependencies": { "jest": ">=24.0.0", "typescript": ">=3.8.0" }, "dependencies": { "@uiw/react-textarea-code-editor": "^2.1.1", "dependency-cruiser": "^12.9.0", "react": "^18.0.0", "react-dom": "^18.0.0", "recoil": "^0.7.2", "redux": "^4.0.5", "styled-components": "^5.3.6", "zustand": "^4.1.1" }, "devDependencies": { "@babel/core": "^7.11.6", "@babel/preset-env": "^7.11.5", "@babel/preset-react": "^7.10.4", "@babel/preset-typescript": "^7.10.4", "@testing-library/react": "^13.1.1", "@types/node": "^14.11.2", "@types/react": "^18.0.6", "@types/react-dom": "^18.0.2", "@types/styled-components": "^5.1.26", "babel-jest": "^26.3.0", "coveralls": "^3.1.0", "css-loader": "^6.7.3", "eslint-config-airbnb-typescript": "^17.0.0", "jest": "^26.6.3", "react-test-renderer": "^18.0.0", "typescript": "^4.0.3" } } ================================================ FILE: package/recoil_generator/__tests__/api.test.js ================================================ import { ledger } from '../src/utils/ledger.ts'; import { atom, selector, selectorFamily, atomFamily } from '../src/api/api.ts'; // testing the atom describe('atom', () => { // destructuring atoms from ledger interface in utils folder const { atoms } = ledger; it('is a function', () => { expect(typeof atom).toBe('function'); }); it('should update ledger upon invocation', () => { // creating a mock atom atom({ key: 'exampleAtom', default: false, }); // verifying atoms property (array) on ledger has been updated with input atom expect(atoms).toHaveLength(1); }); it('should create Recoil atom with correct key name', () => { // verifying that input atom key matches 'exampleAtom' expect(atoms[0]).toHaveProperty('key', 'exampleAtom'); }); }); describe('selector', () => { // destructuring selectors from ledger object in utils folder const { selectors } = ledger; const test = true; it('is a function', () => { // verify selector is a function expect(typeof selector).toBe('function'); }); it('should update ledger upon invocation', () => { // creating a mock selector with key, get, set selector({ key: 'exampleSelector', get: () => 'getMethod', set: () => 'setMethod', }); // verify selectors property in ledger has been updated with mock selector expect(selectors).toHaveLength(1); }); // verifying that input selector key matches 'exampleSelector' it('should capture correct key name', () => { expect(selectors[0]).toEqual('exampleSelector'); }); xit('should return an object if an input condition evaluates to true', () => { // verify that selector (recoilSelector in this context) invocation returns an object expect(typeof selector(test)).toBe('object'); }); }); describe('atomFamily', () => { it('should return a function', () => { // create a mock atomFamily const familyFactory = atomFamily({ key: 'familyKey', default: (param) => param.toString(), }); // verify that familyFactory is a function expect(typeof familyFactory).toEqual('function'); }); }); describe('selectorFamily', () => { // truthy parameter const test = true; it('should return a function', () => { // create a mock selectorFamily const familyFactory = selectorFamily({ key: 'familyKey', get: () => () => 'some value', set: () => () => undefined, default: (param) => param.toString(), }); // verify that familyFactory is a function expect(typeof familyFactory).toEqual('function'); }); it('should return an object if an input condition evaluates to true', () => { // verify that selectorFamily (recoilSelectorFamily in this context) invocation returns an object expect(typeof selectorFamily(test)).toBe('function'); }); }); ================================================ FILE: package/recoil_generator/__tests__/component-utils.test.js ================================================ import { ledger } from '../src/utils/ledger.ts'; import {generateFile} from '../src/component/component-utils'; // Testing generateFile xdescribe('generateFile', () => { const setFile = 0; const array = [[], [], []]; let storeMap = new Map(array); const { atoms, selectors, setters, atomFamilies, selectorFamilies, initialRender, initialRenderFamilies, transactions, setTransactions, } = ledger; // 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 generateFile(setFile, storeMap); }); ================================================ FILE: package/recoil_generator/__tests__/component.test.js ================================================ import React from 'react'; import { RecoilRoot, useRecoilState } from 'recoil'; import { render } from '@testing-library/react'; import { ChromogenObserver } from '../src/component/ChromogenObserver.tsx'; import { ledger } from '../src/utils/ledger.ts'; import { atom } from '../src/api/api.ts'; // import {shallow} from 'enzyme'; // import {mount} from 'enzyme'; describe('chromogenObserver', () => { global.URL = { createObjectURL: () => 'http://mockURL.com', }; beforeEach(() => { console.error = jest.fn(); // creating a mockAtom const mockAtom = atom({ key: 'mockAtom', default: true }); // create a functional mockComponent const MockComponent = () => { // declaring a React Hook using mockAtom as recoilState const [mock, setMock] = useRecoilState(mockAtom); // render a mock-button that toggles mock recoilState onclick return