Showing preview only (255K chars total). Download the full file or copy to clipboard to get everything.
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
<!--- What types of changes does your code introduce to Scratch Project? Put an `x` in the boxes that apply. -->
- [ ] 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
<!--- Describe the problem or feature. Link to the issue(s) fixed by this pull request if applicable. -->
## Approach
<!--- How does your change address the problem? -->
## Resources
<!--- Describe the research stage. Link to any blog posts, video, patterns, libraries, addons, or other resources that helped you to solve this problem. -->
## Screenshot(s)
<!--- (if applicable--you can delete otherwise) -->
<!--- Include a screenshot here if the change you made changes the look of the site in any way! -->
================================================
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
================================================
<div align="center">
<a href="https://chromogen-site-eight.vercel.app/">
<img
height="200"
width="450"
alt="Chromogen logo"
src="./assets/logo/Chromogen.png"
/>
</a>
<h3>A UI-driven test-generation package for <a href= https://github.com/pmndrs/zustand> Zustand</a> Stores and <a href="https://github.com/facebookexperimental/Recoil">Recoil.js</a> selectors.</h3>
<br />
[](https://www.npmjs.com/package/chromogen)
[](https://github.com/open-source-labs/Chromogen/blob/master/LICENSE)
[](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)
[](http://makeapullrequest.com)
[](https://www.npmjs.com/package/chromogen)
[](https://github.com/open-source-labs/Chromogen)
<br />
</div>
<br />
## 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)
<Br><br />
## 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.
<br><Br>
[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.
<b> Chromogen is now compatible with React V18! </b>
<br><hr>
## Supported Tests
<b>Zustand Tests</b>
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.
<br>
<b>Recoil Tests</b>
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.
<br><hr>
## Installing the Package
```
npm install chromogen
```
<br><hr>
## Installation for Zustand Apps
Before using Chromogen, you'll need to make two changes to your application:
1. Import the `<ChromogenZustandObserver />` component and render it alongside any other components in `<App />`
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 `<App />`.
```jsx
import React from 'react';
import { ChromogenZustandObserver } from 'chromogen';
import TodoList from './TodoList';
const App = () => (
<>
<ChromogenZustandObserver>
<TodoList />
</ChromogenZustandObserver>
</>
);
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;
```
<br><hr>
## Installation for Recoil Apps
Before running Chromogen, you'll need to make two changes to your application:
1. Import the `<ChromogenObserver />` component as a child of `<RecoilRoot />`
1. Import the `atom` and `selector` functions from Chromogen instead of Recoil
<i>Note: These changes do have a small performance cost, so they should be reverted before deploying to production.</i>
<br>
### 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) => (
<RecoilRoot>
<ChromogenObserver />
<MyComponent {...props} />
</RecoilRoot>
);
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';
// ...
<ChromogenObserver store={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';
// ...
<ChromogenObserver store={[atoms, selectors, misc]} />;
```
<br>
### 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';
},
});
```
<br><hr>
## 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.
<div align="center">

</div>
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.
<br><hr>
## 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.
<br><Br>
**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
```
<br><Br>
**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 <reference to the /package directory on your local machine> run build && npm pack <reference to the /package directory on your local machine> && npm uninstall chromogen && npm install ./chromogen-5.0.1.tgz && npm start"
```
<br><hr>
## Test Setup
### Zustand Test Setup
Before running the test file, you'll need to specify the import path for your store by replacing `<ADD STORE FILEPATH>`. 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** |
| :-------------------------------------------------------------------: | :-------------------------------------------------------------------: |
|  |  |
<div align="center">

</div>
<br>
---
### Recoil Test Setup
Before running the test file, you'll need to specify the import path for your store by replacing `<ADD STORE FILEPATH>`. 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** |
| :-----------------------------------------------------------: | :----------------------------------------------------------: |
|  |  |
You're now ready to run your tests! Upon running your normal Jest test command, you should see three suites for `chromogen.test.js`:
<div align="center">

</div>
**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.
<br><hr>
## 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 <a href='https://demo-zustand-todo.vercel.app/'>here</a>, and view the source code in the `demo-zustand-todo` folder of this repository.
<br>
### 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.
<br><hr>
## 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`.
<br><Br>
## Chromogen V5.0 updates
<br><hr>
**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
<br><Br><br><hr>
**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.
<br><Br><br><hr>
**CI/CD overhaul**
**Why?**
*Travis implementation not functional
**What?**
*Re-implemented CI/CD with Jenkins
<br><Br><br><hr>
**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
<br> <br> <br> <hr>
## Core Team
<br>
<table>
<tr align="center">
<td align="center"><a href="https://github.com/sirbrachthepale"><img src="https://ca.slack-edge.com/T047AGRDFG8-U04EFS600F2-6758e04a3dcc-512" width="100px;" alt=""/><br /><sub><b>Brach Burdick</b></sub></a></td>
<td align="center"><a href="https://github.com/dnvt"><img src="https://avatars.githubusercontent.com/u/60344684?s=400&u=7fa22ae1486df42eaf172c3f08941416603387c0&v=4" width="100px;" alt=""/><br /><sub><b>Francois Denavaut</b></sub></a></td>
<td align="center"><a href="https://github.com/maggiekwan"><img src="https://ca.slack-edge.com/T047AGRDFG8-U046ZLFULCC-533eb79ef8c7-512" width="100px;" alt=""/><br /><sub><b>Maggie Kwan</b></sub></a></td>
<td align="center"><a href="https://github.com/Lawliang"><img src="https://ca.slack-edge.com/T047AGRDFG8-U04CQEBF85B-267e9eba74d2-512" width="100px;" alt=""/><br /><sub><b>Lawrence Liang</b></sub></a></td>
<td align="center"><a href="https://github.com/michellebholland"><img src="https://avatars3.githubusercontent.com/u/64747593" width="100px;" alt=""/><br /><sub><b>Michelle Holland</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/andywang23"><img src="https://avatars1.githubusercontent.com/u/64433815" width="100px;" alt=""/><br /><sub><b>Andy Wang</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/connorrose"><img src="https://avatars1.githubusercontent.com/u/42079810" width="100px;" alt=""/><br /><sub><b>Connor Rose Delisle</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/chenchingk"><img src="https://avatars0.githubusercontent.com/u/40308081" width="100px;" alt=""/><br /><sub><b>Jim Chen</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/amyy98"><img src="https://avatars.githubusercontent.com/u/68040348?v=4" width="100px;" alt=""/><br /><sub><b>Amy Yee</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/wlstjs"><img src="https://avatars1.githubusercontent.com/u/68680285?s=400&u=5b89d376d4d27a77442b74dcfe1c9c4025ce6453&v=4" width="100px;" alt=""/><br /><sub><b>Jinseon Shin</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/rtumel123"><img src="https://i.postimg.cc/MGDTWMhQ/Ryan.jpg" width="100px;" alt=""/><br /><sub><b>Ryan Tumel</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/cgreer011"><img src="https://i.postimg.cc/qMPgQdsz/cam.jpg" width="100px;" alt=""/><br /><sub><b>Cameron Greer</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/nicholasjs"><img src="https://avatars.githubusercontent.com/u/59386257?v=4" width="100px;" alt=""/><br /><sub><b>Nicholas Shay</b></sub></a></td>
</tr>
<tr align="center">
<!-- SPACE -->
<td align="center"><a href="https://github.com/mp-04"><img src="https://i.postimg.cc/nz6GjXXV/mp.jpg" width="100px;" alt=""/><br /><sub><b>Marcellies Pettiford</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/smk53664"><img src="https://i.postimg.cc/mrRkfN64/sung.jpg" width="100px;" alt=""/><br /><sub><b>Sung Kim</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/lina4lee"><img src="https://i.postimg.cc/bJwvdYhF/lina.jpg" width="100px;" alt=""/><br /><sub><b>Lina Lee</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/ericaysoh"><img src="https://i.postimg.cc/76tZzvPP/erica.jpg" width="100px;" alt=""/><br /><sub><b>Erica Oh</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/dtalmaraz"><img src="https://avatars.githubusercontent.com/u/94757231?v=4" width="100px;" alt=""/><br /><sub><b>Dani Almaraz</b></sub></a></td>
<!-- SPACE -->
<tr align="center">
<td align="center"><a href="https://github.com/crgb0s"><img src="https://i.postimg.cc/BbpvdSJD/craig.jpg" width="100px;" alt=""/><br /><sub><b>Craig Boswell</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/Hali3030"><img src="https://i.postimg.cc/xTj7Yf0C/hussein.jpg" width="100px;" alt=""/><br /><sub><b>Hussein Ahmed</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/iannkila"><img src="https://i.postimg.cc/rwcBD1C8/ian.jpg" width="100px;" alt=""/><br /><sub><b>Ian Kila</b></sub></a></td>
<!-- SPACE -->
<td align="center"><a href="https://github.com/yuehaowong"><img src="https://i.postimg.cc/T2JqRnwj/yuehao.jpg" width="100px;" alt=""/><br /><sub><b>Yuehao Wong</b></sub></a></td>
</tr>
</tr>
</table>
<br><br>
## 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
================================================
<div align="center">
# The official demo app for [Chromogen](https://github.com/oslabs-beta/Chromogen).

</div>
## 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 = () => (
<RecoilRoot>
<ChromogenObserver store={[selectors, atoms]} />
<TodoList />
</RecoilRoot>
);
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 (
<>
<div id="quoteContainer">
<p>{quoteText}</p>
<button type="button" onClick={() => setQuoteNumber(Math.floor(Math.random() * 1643))}>
New Quote
</button>
</div>
</>
);
};
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) ? (
<div className={checkBoxClasses[item.priority] || 'itemContainer'} id="todoItem">
<input type="text" value={item.text} readOnly />
<Checkbox
disableRipple
checked={todoList.find((todo) => todo.id === item.id).isComplete}
color="default"
inputProps={{ 'aria-label': 'primary checkbox' }}
style={{ cursor: 'default' }}
/>
</div>
) : 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 (
<div className="searchContainer">
<input
className="searchField"
placeholder="Search for a Todo"
type="text"
value={searchText || searchState.searchTerm}
onChange={onSearchTextChange}
onLoad={onSearchTextChange}
/>
<select className="prioritySelect" onChange={onSelectChange}>
<option value="all">All Priorities</option>
<option value="high">High Priority</option>
<option value="medium">Medium Priority</option>
<option value="low">Low Priority</option>
</select>
<div className="searchResults">
{searchState.results.map((result, idx) => (
<ReadOnlyTodoItem key={idx} item={result} />
))}
</div>
</div>
);
};
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 (
<div className={checkBoxClasses[item.priority] || 'itemContainer'} id="todoItem">
<input type="text" value={item.text} onChange={editItemText} />
<Checkbox
disableRipple
checked={item.isComplete}
color="default"
inputProps={{ 'aria-label': 'primary checkbox' }}
onChange={toggleItemCompletion}
/>
<button type="submit" onClick={deleteItem}>
X
</button>
</div>
);
};
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) => <Radio style={{ color: 'green' }} size="small" {...props} />;
const YellowRadio = (props) => <Radio style={{ color: 'yellow' }} size="small" {...props} />;
const RedRadio = (props) => <Radio style={{ color: 'red' }} size="small" {...props} />;
return (
<div className="itemCreator">
<input
className="inputText"
placeholder="What needs to be done?"
type="text"
value={inputValue}
onChange={onChange}
/>
<span id="radioContainer">
<FormControl component="fieldset">
<FormLabel color="secondary" component="label" />
<RadioGroup
row
aria-label="priority"
name="priority1"
value={priorityValue}
onChange={handleChange}
>
<FormControlLabel control={<RedRadio />} value="high" />
<FormControlLabel control={<YellowRadio />} value="medium" />
<FormControlLabel control={<GreenRadio />} value="low" />
</RadioGroup>
</FormControl>
</span>
<button type="submit" onClick={addItem}>
Add
</button>
</div>
);
};
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 (
<div className="mainContainer">
<div className="row quoteBox">
<React.Suspense fallback={<small>Loading...</small>}>
<Quotes />
</React.Suspense>
</div>
<div className="row todosDisplayRow">
<h1>Totally Todos!</h1>
<div className="todosContainer">
<TodoQuickCheck />
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
<TodoListFilters />
</div>
<SearchBar />
</div>
<div className="row" />
</div>
);
};
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 (
<ul>
<button
className="filter-button"
id="filterBtn1"
style={{ color: filter === 'Show All' ? '#af6358' : 'whitesmoke' }}
type="submit"
value="Show All"
onClick={updateFilter}
>
All <span> {totalNum || ''}</span>
</button>
<button
className="filter-button"
id="filterBtn2"
style={{ color: filter === 'Show Uncompleted' ? '#af6358' : 'whitesmoke' }}
type="submit"
value="Show Uncompleted"
onClick={updateFilter}
>
Active <span>{totalUncompletedNum || ''}</span>
</button>
<button
className="filter-button"
style={{ color: filter === 'Show Completed' ? '#af6358' : 'whitesmoke' }}
type="submit"
value="Show Completed"
onClick={updateFilter}
>
Complete <span>{totalCompletedNum || ''}</span>
</button>
<button id={sortIconColor[sort]} type="submit" onClick={toggleSort}>
<SortIcon />
</button>
<button id="unsortedGray" type="submit" onClick={toggleDisplayStats}>
{displayStats && totalNum ? (
<span id="statsSpan">
<span id="highSpan">{high || 0}</span>
<span id="mediumSpan">{medium || 0}</span>
<span id="lowSpan">{low || 0}</span>
</span>
) : (
<EqualizerIcon />
)}
</button>
<button id="unsortedGray" type="submit" onClick={reset}>
<RefreshIcon />
</button>
</ul>
);
};
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 && (
<div id="quickCheck">
<Checkbox
disableRipple
checked={allComplete}
color="default"
inputProps={{ 'aria-label': 'primary checkbox' }}
onChange={() => setAllComplete(!allComplete)}
/>
all
</div>
)
);
};
export default TodoQuickCheck;
================================================
FILE: demo-todo/src/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://fonts.googleapis.com/css2?family=Palanquin:wght@300&display=swap"
rel="stylesheet"
/>
<title>Chromogen To-Do Demo</title>
<link rel="icon" type="image/x-icon" href="./favicon.ico" />
</head>
<body>
<div id="app">
<script src="./bundle.js"></script>
</div>
</body>
</html>
================================================
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(
<App />
);
================================================
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 = () => (
<>
<ChromogenZustandObserver>
<TodoList />
</ChromogenZustandObserver>
</>
);
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 (
<>
<div id="quoteContainer">
<p>{quoteText}</p>
<a onClick={() => fetchMe()}>New Quote</a>
</div>
</>
);
};
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) ? (
<div className={checkBoxClasses[item.priority] || 'itemContainer'} id="todoItem">
<input type="text" value={item.text} readOnly />
<Checkbox
disableRipple
checked={todoList.find((todo) => todo.id === item.id).isComplete}
color="default"
inputProps={{ 'aria-label': 'primary checkbox' }}
style={{ cursor: 'default' }}
/>
</div>
) : 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 (
<div className="searchContainer">
<input
className="searchField"
placeholder="Search for a Todo"
type="text"
value={searchText || searchResults.searchTerm}
onChange={onSearchTextChange}
onLoad={onSearchTextChange}
/>
<select
style={{ WebkitAppearance: 'none' }}
className="prioritySelect"
onChange={onSelectChange}
>
<option value="all">All Priorities</option>
<option value="high">High Priority</option>
<option value="medium">Medium Priority</option>
<option value="low">Low Priority</option>
</select>
<div className="searchResults">
{searchResults.results.map((result, idx) => (
<ReadOnlyTodoItem key={idx} item={result} />
))}
</div>
</div>
);
};
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 (
<div className={checkBoxClasses[item.priority] || 'itemContainer'} id="todoItem">
<input
type="text"
value={item.text}
onChange={(e) => editItemText(e.target.value, item.id)}
/>
<Checkbox
disableRipple
checked={item.isComplete}
color="default"
inputProps={{ 'aria-label': 'primary checkbox' }}
onClick={() => toggleItemCompletion(item.id)}
/>
<button type="submit" onClick={() => deleteTodoListItem(item.id)}>
X
</button>
</div>
);
};
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) => <Radio style={{ color: 'green' }} size="small" {...props} />;
const YellowRadio = (props) => <Radio style={{ color: 'yellow' }} size="small" {...props} />;
const RedRadio = (props) => <Radio style={{ color: 'red' }} size="small" {...props} />;
return (
<div className="itemCreator">
<input
className="inputText"
placeholder="What needs to be done?"
type="text"
value={inputValue}
onChange={onChange}
/>
<span id="radioContainer">
<FormControl component="fieldset">
<FormLabel color="secondary" component="label" />
<RadioGroup
row
aria-label="priority"
name="priority1"
value={priorityValue}
onChange={handleChange}
>
<FormControlLabel control={<RedRadio />} value="high" />
<FormControlLabel control={<YellowRadio />} value="medium" />
<FormControlLabel control={<GreenRadio />} value="low" />
</RadioGroup>
</FormControl>
</span>
<button type="submit" onClick={addItem}>
Add
</button>
</div>
);
};
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 (
<div className="mainContainer">
<div className="wrapper">
<center>
<img
id="newChromogenLogo"
src="https://i.postimg.cc/sgXkWQmt/Chromogen-1.png"
alt="this is supposed to be our logo"
/>
</center>
<div className="quoteBox">
<React.Suspense fallback={<small>Loading...</small>}>
<Quotes />
</React.Suspense>
</div>
<div className="todosDisplayRow">
<h1>To-Do List</h1>
<div className="todosContainer">
<TodoQuickCheck />
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
<TodoListFilters />
</div>
</div>
<SearchBar />
<div className="row" />
</div>
</div>
);
};
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 (
<ul>
<button
className="filter-button"
id="filterBtn1"
style={{ color: todoListFilterState === 'Show All' ? '#af6358' : 'whitesmoke' }}
type="submit"
value="Show All"
onClick={updateFilter}
>
All <span> {totalNum || ''}</span>
</button>
<button
className="filter-button"
id="filterBtn2"
style={{ color: todoListFilterState === 'Show Uncompleted' ? '#af6358' : 'whitesmoke' }}
type="submit"
value="Show Uncompleted"
onClick={updateFilter}
>
Active <span>{totalUncompletedNum || ''}</span>
</button>
<button
className="filter-button"
style={{ color: todoListFilterState === 'Show Completed' ? '#af6358' : 'whitesmoke' }}
type="submit"
value="Show Completed"
onClick={updateFilter}
>
Complete <span>{totalCompletedNum || ''}</span>
</button>
<button id={sortIconColor[todoListSortState]} type="submit" onClick={toggleSort}>
<SortIcon />
</button>
<button id="unsortedGray" type="submit" onClick={toggleDisplayStats}>
{displayStats && totalNum ? (
<span id="statsSpan">
<span id="highSpan">{high || 0}</span>
<span id="mediumSpan">{medium || 0}</span>
<span id="lowSpan">{low || 0}</span>
</span>
) : (
<EqualizerIcon />
)}
</button>
<button id="unsortedGray" type="submit" onClick={reset}>
<RefreshIcon />
</button>
</ul>
);
};
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 (
<div id="quickCheck">
<Checkbox
disableRipple
checked={checkBox}
color="default"
inputProps={{ 'aria-label': 'primary checkbox' }}
onClick={() => setAllComplete()}
/>
All
</div>
);
};
export default TodoQuickCheck;
================================================
FILE: demo-zustand-todo/src/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://fonts.googleapis.com/css2?family=Palanquin:wght@300&display=swap"
rel="stylesheet"
/>
<title>Chromogen Zustand Demo To-Do</title>
<link rel="icon" type="image/x-icon" href="./favicon.ico" />
</head>
<body>
<div id="app">
<script src="./bundle.js"></script>
</div>
</body>
</html>
================================================
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(<App />);
================================================
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,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='%238C98F2'><polygon points='0,0 100,0 50,50'/></svg>")
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
================================================
<div align="center">
<h1>Chromogen</h1>
<a href="https://github.com/open-source-labs/Chromogen">
<img
height="120"
width="120"
alt="chromogen logo"
src="https://github.com/open-source-labs/Chromogen/raw/master/assets/logo/Chromogen.png"
/>
</a>
<h3>A UI-driven Jest test-generation package for <a href="https://www.npmjs.com/package/recoil">Recoil.js</a> selectors and <a href="https://www.npmjs.com/package/zustand">Zustand</a> store hooks.</h3>
<br />
[](https://www.npmjs.com/package/chromogen)
[](https://github.com/open-source-labs/Chromogen/blob/master/LICENSE)
<br />
</div>
<br />
**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.
<br /><br /><br />
## Installation for Zustand Apps
Before using Chromogen, you'll need to make two changes to your application:
1. Import the `<ChromogenZustandObserver />` component and render it alongside any other components in `<App />`
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 `<App />`.
```jsx
import React from 'react';
import { ChromogenZustandObserver } from 'chromogen';
import TodoList from './TodoList';
const App = () => (
<>
<ChromogenZustandObserver />
<TodoList />
</>
);
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;
```
<br><hr>
## Installation for Recoil Apps
Before running Chromogen, you'll need to make two changes to your application:
1. Import the `<ChromogenObserver />` component as a child of `<RecoilRoot />`
1. Import the `atom` and `selector` functions from Chromogen instead of Recoil
<i>Note: These changes do have a small performance cost, so they should be reverted before deploying to production.</i>
<br>
### 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) => (
<RecoilRoot>
<ChromogenObserver />
<MyComponent {...props} />
</RecoilRoot>
);
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';
// ...
<ChromogenObserver store={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';
// ...
<ChromogenObserver store={[atoms, selectors, misc]} />;
```
<br>
### 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';
},
});
```
<br><hr>
## 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.
<div align="center">

</div>
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.
<br><hr>
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 <button id="mock-button" type="button" onClick={() => setMock(!mock)} />;
};
render(
<RecoilRoot>
<ChromogenObserver />
<MockComponent />
</RecoilRoot>,
);
});
// ChromogenObserver lines 25-36
it('should relay messages to DevTool', () => {
// expect(window.addEventListener('message', 'connectChromogen')).toBeTruthy()
// expect(window.addEventListener('message', 'downloadFile')).toBeTruthy()
// expect(window.addEventListener('message', 'toggleRecord')).toBeTruthy()
});
// ChromogenObserver lines 56-80
// Store contains atoms and selectors
it('should update storeMap with all items from passed in store', () => {
})
// ChromogenObserver lines 104-117
it('should update atomFamilyState', () => {
})
// ChromogenObserver lines 142-145
it('should ', () => {
})
// ChromogenObserver lines 154-155
it('should change button color on mouse enter/leave', () => {
})
it('should render a download link', () => {
// verify that download Chromogen tests link exists and is being rendered
expect(document.getElementById('chromogen-download')).toBeTruthy();
});
it('should render two buttons by default', () => {
// verify that generate-file and record buttons are being rendered
expect(document.getElementById('chromogen-generate-file')).toBeTruthy();
expect(document.getElementById('chromogen-toggle-record')).toBeTruthy();
});
xit('should create a file URL on button click', () => {
// invoking a click on generate-file button
document.getElementById('chromogen-generate-file').click();
// declaring a const downloadLink referencing hidden download link
const downloadLink = document.getElementById('chromogen-download');
// verify that download link has a URL that is being referenced
expect(downloadLink.getAttribute('href')).toBeTruthy();
});
it('should create transactions when state updates', () => {
// invoking a click to check that state in mock-button is being toggled (in the state mock)
document.getElementById('mock-button').click();
// Using Promise to get around async nature of Recoil transactions
expect(
// verifying that resolve (mockData) has been added to ledger.transactions
new Promise((resolve) => setTimeout(() => resolve(ledger.transactions), 100)),
// verify that resolve property on promise object has been updated
).resolves.toHaveLength(1);
});
});
================================================
FILE: package/recoil_generator/__tests__/core-utils.test.jx
================================================
import {
debouncedAddToTransactions,
wrapGetter,
wrapSetter,
} from '../src/api/core-utils';
import { debounce } from '../src/utils/utils';
xdescribe('debouncedAddToTransaction', () => {
});
xdescribe('wrapGetter', () => {
});
xdescribe('wrapSetter', () => {
});
================================================
FILE: package/recoil_generator/__tests__/output-utils.test.js
================================================
import {
initializeAtoms,
assertState,
testSelectors,
testSetters,
importRecoilFamily,
atomFamilyHook,
//writeableHook,
readableHook,
} from '../src/output/output-utils.ts';
// testing ternary operator in initializeAtoms helper function
describe('initializeAtoms', () => {
// create mock atomUpdate object, follows AtomUpdate interface
const atomUpdate = {
key: 'testAtom',
value: 2,
previous: 1,
updated: true,
};
it('should set correct atom value if current is true', () => {
// create variable to hold evaluated result of invoking initializeAtoms on the mock array with a parameter of true
const returnString = initializeAtoms([atomUpdate], true);
// verify that returnString contains key property of a string and contains a value property on the atomUpdate object (since true was passed into 'initializeAtoms')
expect(returnString).toEqual(
expect.stringContaining(`result.current.set${atomUpdate.key}(${atomUpdate.value})`),
);
});
it('should set correct atom value if current is false', () => {
// create variable to hold evaluated result of invoking initializeAtoms on the mock array with a parameter of false
const returnString = initializeAtoms([atomUpdate], false);
// verify that returnString contains key property of a string and contains a value property on the atomUpdate object (since false was passed into 'initializeAtoms')
expect(returnString).toEqual(
expect.stringContaining(`result.current.set${atomUpdate.key}(${atomUpdate.previous})`),
);
});
});
describe('assertState', () => {
// create mock selectors array, follows SelectorUpdate interface
const selectorUpdates = [
{
key: 'testSelector1',
value: true,
},
{
key: 'testSelector2',
value: 100,
},
];
it('should assert on each selector value', () => {
// create variable to hold evaluated result of invoking assertState on mock array
const returnString = assertState(selectorUpdates);
// verify that output test contains a string checking that the object's key equals a stringified version of its value on the same object
expect(returnString).toEqual(
expect.stringContaining(
`expect(result.current.${selectorUpdates[0].key}Value).toStrictEqual(${JSON.stringify(
selectorUpdates[0].value,
)});`,
),
);
expect(returnString).toEqual(
expect.stringContaining(
`expect(result.current.${selectorUpdates[1].key}Value).toStrictEqual(${JSON.stringify(
selectorUpdates[1].value,
)});`,
),
);
});
});
describe('importRecoilFamily', () => {
const familyObj = {
familyName: 'string',
atomName: 'test',
};
it('should return a string with an object as its parameter', () => {
expect(typeof importRecoilFamily(familyObj)).toBe('string');
});
});
describe('readableHook', () => {
const keyArray = ['one', 'two', 'chromogen'];
it('should return a string', () => {
expect(typeof readableHook(keyArray)).toBe('string');
});
});
// describe('writeableHook', () => {
// const keyArray = ['chromo', 'gen', 'chromogen'];
// it('should return a string', () => {
// expect(typeof writeableHook(keyArray)).toBe('string');
// });
// });
describe('testSelectors', () => {
it('should scrub special characters from key names', () => {
// create instance of invoking testSelectors on mock array that follows the Transaction interface
const returnString = testSelectors([
{
state: [
{
key: 'atom1',
value: 1,
previous: 2,
updated: true,
},
],
updates: [
{
key: 'selector1',
value: 3,
},
],
atomFamilyState: [
{
family: 'familyName1',
key: 'spec!alCh@r',
value: 4,
updated: true,
},
],
familyUpdates: [
{
key: 'familyUpdate1',
value: 5,
params: 'params',
},
],
},
]);
// verify that if key property's value is a string with special characters they will be removed
expect(returnString).toEqual(expect.not.stringContaining('spec!alCh@r'));
});
});
// covers branch test percentage in testSetters
describe('testSetters', () => {
// create mock array with setter object
const setTransactionsArrayWithSetter = [
{
state: [
{
key: 'atom1',
value: 1,
previous: 0,
updated: true,
},
],
setter: {
key: 'selector1',
value: 2,
params: 'spec!alCh@r',
},
},
];
// create mock array without setter object
const setTransactionsArrayWithoutSetter = [
{
state: [
{
key: 'atom1',
value: 1,
previous: 0,
updated: true,
},
],
},
];
const truthyReturnString = testSetters(setTransactionsArrayWithSetter);
const falsyReturnString = testSetters(setTransactionsArrayWithoutSetter);
it('should scrub special characters from params', () => {
// verify that if params property's value is a string with special characters, they will be removed
expect(truthyReturnString).toEqual(expect.not.stringContaining('spec!alCh@r'));
});
it('should return a string if an array is passed in', () => {
// verify that a string is returned if provided an array with out a setter object
expect(typeof falsyReturnString).toBe('string');
});
});
// TEST FOR ATOMFAMILYHOOK lines 70-81
//create mock transactionArray
xdescribe('atomFamilyHook', () => {
const transactionArray = [
{
atomFamilyState: [
{
key: 'spec!alCh@rspec!alCh@r',
family: 'familyName',
value: 10,
updated: true,
},
],
familyUpdates: [
{
key: 'familyUpdate1',
value: 5,
params: 'params',
},
],
},
]; // truthy
const transactionArray2 = []; // falsy
const truthyReturnStr = atomFamilyHook(transactionArray);
const falsyReturnStr = atomFamilyHook(transactionArray2);
it('should scrub special characters', () => {
expect(truthyReturnStr).toEqual(expect.not.stringContaining('spec!alCh@r'));
});
it('should return empty string when transactionsArr length is falsy', () => {
expect(falsyReturnStr).toBe('');
});
});
================================================
FILE: package/recoil_generator/__tests__/output.test.js
================================================
import { setFilter, output } from '../src/output/output.ts';
// testing setFilter function
describe('setFilter', () => {
it('should remove setter keys from array of selector keys', () => {
// create mock selectors array
const selectors = ['one', 'two', 'three'];
// create mock setters array
const setters = ['one'];
// store evaluated result of invoking setFilter on the mock data in an array
const filtered = setFilter(selectors, setters);
// verify that the info from setters caused a matching value to be removed from the selectors array ('one')
expect(filtered).not.toContain('one');
});
});
describe('output', () => {
it('should return a string', () => {
// create mock ledger object
const mockLedger = {
atoms: [],
selectors: [],
setters: [],
atomFamilies: [],
selectorFamilies: [],
initialRender: [],
initialRenderFamilies: [],
transactions: [],
setTransactions: [],
};
// verify that type of mockLedger is a string after output function is invoked on it
expect(typeof output(mockLedger)).toEqual('string');
});
});
================================================
FILE: package/recoil_generator/__tests__/utils.test.js
================================================
import { debounce, convertFamilyTrackerKeys } from '../src/utils/utils.ts';
jest.useFakeTimers();
describe('debounce', () => {
it('should return a new function', () => {
// declare mock function that returns a string
const inputFunction = () => 'example';
// declare a function that stores the evaluated result of invoking debounce on inputFunction with a wait time of 0s
const outputFunction = debounce(inputFunction, 0);
// verify that outputFunction is a function
expect(typeof outputFunction).toBe('function');
// verify that that the 'debounced function' (outputFunction) is different than the parameter function (inputFunction)
expect(outputFunction).not.toBe(inputFunction);
});
it('should limit consecutive calls', () => {
// increment count to 1 after 100ms
let count = 0;
const increment = debounce(() => {
count += 1;
}, 100);
// invoke increment twice
increment();
increment();
// advance timer to 101ms using a mock jest function
jest.advanceTimersByTime(101);
// verify that count only incremented once because it was debounced
expect(count).toEqual(1);
});
});
// testing convertFamilyTrackerKeys
describe('convertFamilyTrackerKeys', () => {
it('should update key names if in map', () => {
// create mock tracker (object)
const newTracker = convertFamilyTrackerKeys(
// first parameter is an object with a property whose value is a string
{ keyOne: 'some value' },
// second parameter is a new Map
new Map([['keyOne', 'keyUpdated']]),
);
// verify that newTracker object includes property from Map (keyUpdated)
expect(newTracker).toHaveProperty('keyUpdated');
// verify that newTracker did not update key name with first parameter
expect(newTracker).not.toHaveProperty('keyOne');
});
it('should preserve key names if not in map', () => {
// create mock tracker (object)
const newTracker = convertFamilyTrackerKeys(
{ keyOne: 'some value' },
new Map([['keyTwo', 'keyNotUpdated']]),
);
// verify that newTracker's first parameter has been preserved
expect(newTracker).toHaveProperty('keyOne');
// verify that newTracker does have keyTwo in first parameter
expect(newTracker).not.toHaveProperty('keyTwo');
});
});
================================================
FILE: package/recoil_generator/src/api/api.ts
================================================
/* eslint-disable */
import type {
RecoilState,
RecoilValueReadOnly,
AtomOptions,
ReadWriteSelectorOptions,
ReadOnlySelectorOptions,
SerializableParam,
AtomFamilyOptions,
ReadWriteSelectorFamilyOptions,
ReadOnlySelectorFamilyOptions,
} from 'recoil';
import type { SelectorConfig, SelectorFamilyConfig } from '../types';
import {
selector as recoilSelector,
atom as recoilAtom,
atomFamily as recoilAtomFamily,
selectorFamily as recoilSelectorFamily,
} from 'recoil';
import { wrapGetter, wrapSetter } from './core-utils';
import { dummyParam } from '../utils/utils';
import { ledger } from '../utils/ledger';
import { wrapFamilyGetter, wrapFamilySetter } from './family-utils';
/* eslint-enable */
/**
* If transactions.length is greater than 1, the selector is being created after the initial render
* (i.e. a dynamically generated selector) and will not be tracked. Doing so would break the imports
* and assertions within the output test file. Same logic is applied to new atoms.
*
* If get is undefined, native Async, or Babel-transpiled generator-based async (id'd via RegEx),
* we don't do any injecting or tracking. Selector just gets created & returned back out.
*
* Otherwise, we attempt to wrap get & set methods with custom functions that log the return
* value on each transaction to the corresponding ledger array.
*
* If get returns a promise on page load, we delete selector from the selectors array
* and do not track it on subsequent calls (using "returnedPromise" flag, since we can't "un-inject").
*/
/* ----- SELECTOR ----- */
export function selector<T>(options: ReadWriteSelectorOptions<T>): RecoilState<T>;
export function selector<T>(options: ReadOnlySelectorOptions<T>): RecoilValueReadOnly<T>;
// Overload function signature
export function selector(config: ReadWriteSelectorOptions<any> | ReadOnlySelectorOptions<any>) {
const { key, get } = config;
const { transactions, selectors, setters } = ledger;
if (
transactions.length > 0
|| !get
|| get.constructor.name === 'AsyncFunction'
|| get.toString().match(/^\s*return\s*_.*\.apply\(this, arguments\);$/m)
) {
return recoilSelector(config);
}
// Wrap get method with tracking logic & update config
const getter = wrapGetter(key, get);
const newConfig: SelectorConfig<any> = { key, get: getter };
// Add setter to newConfig only if set method is defined
if ('set' in config) {
const setter = wrapSetter(key, config.set);
newConfig.set = setter;
setters.push(key);
}
// Create selector & add to ledger
const trackedSelector = recoilSelector(newConfig);
selectors.push(trackedSelector.key);
return trackedSelector;
}
/* ----- ATOM ----- */
export function atom<T>(config: AtomOptions<T>): RecoilState<T> {
const { transactions, atoms } = ledger;
const newAtom = recoilAtom(config);
// Can't use key-only b/c atoms must be passed to getLoadable during transaction iteration
if (transactions.length === 0) atoms.push(newAtom);
return newAtom;
}
/* ----- ATOM FAMILY ----- */
export function atomFamily<T, P extends SerializableParam>(
config: AtomFamilyOptions<T, P>,
): (params: P) => RecoilState<T> {
const { atomFamilies } = ledger;
const { key } = config;
// Initialize new family in atomFamilies tracker
atomFamilies[key] = {};
return (params: P): RecoilState<T> => {
const strParams = JSON.stringify(params);
// If the atom has already been created, return from cache, otherwise we'll be creating a new
// instance of an atom every time we invoke this func (which can lead to infinite re-render loop)
const cachedAtom = atomFamilies[key][strParams];
if (cachedAtom !== undefined) return cachedAtom;
const newAtomFamilyMember = recoilAtomFamily(config)(params);
// Storing every atom created except for dummy atom created by ChromogenObserver's onload useEffect hook
if (strParams !== dummyParam) atomFamilies[key][strParams] = newAtomFamilyMember;
return newAtomFamilyMember;
};
}
/* ----- SELECTOR FAMILY ----- */
export function selectorFamily<T, P extends SerializableParam>(
options: ReadWriteSelectorFamilyOptions<T, P>,
): (param: P) => RecoilState<T>;
export function selectorFamily<T, P extends SerializableParam>(
options: ReadOnlySelectorFamilyOptions<T, P>,
): (param: P) => RecoilValueReadOnly<T>;
// Overload function signature
export function selectorFamily<T>(
config:
| ReadWriteSelectorFamilyOptions<T, SerializableParam>
| ReadOnlySelectorFamilyOptions<T, SerializableParam>,
) {
const { key, get } = config;
const { transactions, selectorFamilies } = ledger;
// Testing whether returned function from configGet is async
if (
!get
|| transactions.length > 0
|| get(dummyParam).constructor.name === 'AsyncFunction'
|| get(dummyParam)
.toString()
.match(/^\s*return\s*_.*\.apply\(this, arguments\);$/m)
) {
return recoilSelectorFamily(config);
}
const getter = wrapFamilyGetter(key, get);
const newConfig: SelectorFamilyConfig<any, SerializableParam> = { key, get: getter };
let isSettable = false;
if ('set' in config) {
isSettable = true;
const setter = wrapFamilySetter(key, config.set);
newConfig.set = setter;
}
// Create selector generator & add to selectorFamily for test setup
const trackedSelectorFamily = recoilSelectorFamily(newConfig);
selectorFamilies[key] = { trackedSelectorFamily, prevParams: new Set(), isSettable };
return trackedSelectorFamily;
}
================================================
FILE: package/recoil_generator/src/api/core-utils.ts
================================================
/* eslint-disable */
import { debounce } from '../utils/utils';
import { ledger } from '../utils/ledger';
import { recordingState } from '../utils/store';
/* eslint-enable */
const { transactions, initialRender, selectors, setTransactions } = ledger;
const DEBOUNCE_MS = 250;
// Set timeout for selector get calls
const debouncedAddToTransactions = debounce(
(key, value, params) =>
params !== undefined
? transactions[transactions.length - 1].familyUpdates.push({ key, value, params })
: transactions[transactions.length - 1].updates.push({ key, value }),
DEBOUNCE_MS,
);
// the logic for recording selectors only when they fire
// whenever get method is fired, chromogen records
const wrapGetter = (key: string, get: Function) => {
let returnedPromise: boolean = false;
return (utils: any) => {
//will return what normal recoil selector will return aka regular selector method
const value = get(utils);
//Checking whether value is async
// Only capture selector data if currently recording (if record button has been hit)
if (utils.get(recordingState)) {
//making sure no transactions have been fired
if (transactions.length === 0) {
// Promise-validation is expensive, so we only do it once, on initial load
if (typeof value === 'object' && value !== null && value.constructor.name === 'Promise') {
ledger.selectors = selectors.filter((current) => current !== key);
returnedPromise = true;
} else {
initialRender.push({ key, value });
}
} else if (!returnedPromise) {
// Debouncing (throttling) allows TransactionObserver to push to array first
// Length must be computed within debounce to correctly find last transaction
// only capture meaningful function calls
// when called, timer starts; if x amount of time passes and function isnt called again, it fires; if called, resets timer
debouncedAddToTransactions(key, value);
}
}
return value;
};
};
const wrapSetter = (key: string, set: Function) => (utils: any, newValue: any) => {
if (utils.get(recordingState) && setTransactions.length > 0) {
// allow TransactionObserver to push to array first
// Length must be computed after timeout to correctly find last transaction
// this is here b/c of async stuff with useRecoilTransactionObserver
setTimeout(() => {
setTransactions[setTransactions.length - 1].setter = { key, newValue };
}, 0);
}
// returns what regular selector would return (?)
return set(utils, newValue);
};
export {debouncedAddToTransactions, wrapGetter, wrapSetter};
================================================
FILE: package/recoil_generator/src/api/family-utils.ts
================================================
/* eslint-disable */
import type { SerializableParam } from 'recoil';
import { ledger } from '../utils/ledger';
import { dummyParam } from '../utils/utils';
import { recordingState } from '../utils/store';
import { debouncedAddToTransactions } from './core-utils';
/* eslint-enable */
const { transactions, selectorFamilies, initialRenderFamilies, setTransactions } = ledger;
export const wrapFamilyGetter = (key: string, configGet: Function) => {
let returnedPromise = false;
return (params: SerializableParam) => (utils: any) => {
const { get } = utils;
const value = configGet(params)(utils);
// Only capture selector data if currently recording
if (get(recordingState)) {
if (transactions.length === 0) {
// Promise-validation is expensive, so we only do it once, on initial load
if (
typeof value === 'object'
&& value !== null
&& Object.prototype.toString.call(value) === '[object Promise]'
) {
delete selectorFamilies[key];
returnedPromise = true;
} else {
initialRenderFamilies.push({ key, params, value });
}
} else if (!returnedPromise) {
// Track every new params
if (!selectorFamilies[key].prevParams.has(params)) {
selectorFamilies[key].prevParams.add(params);
}
// Debouncing allows TransactionObserver to push to array first
// Length must be computed within debounce to correctly find last transaction
// Excluding dummy selector created by ChromogenObserver's onload useEffect hook
if (params !== dummyParam) debouncedAddToTransactions(key, value, params);
}
}
// Return value from original get method
return value;
};
};
export const wrapFamilySetter = (key: string, set: Function) => (params: SerializableParam) => (
utils: any,
newValue: any,
) => {
if (utils.get(recordingState) && setTransactions.length > 0) {
// allow TransactionObserver to push to array first
// Length must be computed after timeout to correctly find last transaction
setTimeout(() => {
setTransactions[setTransactions.length - 1].setter = { key, params, newValue };
}, 0);
}
return set(params)(utils, newValue);
};
================================================
FILE: package/recoil_generator/src/component/ChromogenObserver.tsx
================================================
/* eslint-disable */
import type { Snapshot } from 'recoil';
import type { AtomFamilyState } from '../types';
import React, { useState, useEffect } from 'react';
import { useRecoilState, useRecoilTransactionObserver_UNSTABLE } from 'recoil';
import { dummyParam } from '../utils/utils';
import { recordingState } from '../utils/store';
import { ledger } from '../utils/ledger';
import { styles, generateFile, generateTests } from './component-utils';
/* eslint-enable */
export const ChromogenObserver: React.FC<{ store?: Array<object> | object }> = ({ store }) => {
// Initializing as undefined over null to match React typing for AnchorHTML attributes
const [file, setFile] = useState<undefined | string>(undefined);
const [storeMap, setStoreMap] = useState<Map<string, string>>(new Map());
const [recording, setRecording] = useRecoilState<boolean>(recordingState);
const [devtool, setDevtool] = useState<boolean>(false);
const [, setEditFile] = useState<undefined | string>(undefined);
// DevTool message handling
const receiveMessage = (message: any) => {
switch (message.data.action) {
case 'connectChromogen':
setDevtool(true);
window.postMessage({ action: 'moduleConnected' }, '*');
break;
case 'downloadFile':
generateFile(setFile, storeMap);
break;
case 'editFile':
const array = generateFile(setEditFile, storeMap);
window.postMessage({ action: 'editFileReceived', data: array }, '*');
break;
case 'toggleRecord':
setRecording(!recording);
window.postMessage({ action: 'setStatus' }, '*');
break;
default:
// Do nothing
}
};
// Add/remove DevTool event listeners
useEffect(() => {
window.addEventListener('message', receiveMessage);
return () => window.removeEventListener('message', receiveMessage);
});
// Auto-click download link when a new file is generated (via button click)
useEffect(() => document.getElementById('chromogen-download')!.click(), [file]);
// ! to get around strict null check in tsconfig
// Update storeMap with src variable names if store prop passed
useEffect(() => {
if (store !== undefined) {
const storeArr = Array.isArray(store) ? store : [store];
const newStore: Map<string, string> = new Map();
storeArr.forEach((storeModule) => {
Object.entries(storeModule).forEach(([variable, imported]: [any, any]) => {
let key;
/** Relevant imports will be either an object (for vanilla atoms or selectors)
* or functions (for atom or selector families). If we are examining a family function,
* we will need to invoke it to create an atom/selector in order to pull the
* original family key out from the generated atom or selector's individual key.
* */
if (typeof imported === 'function') {
// Extended atom fam key will follow format of `[key]__"chromogenDummyParam"__withFallback`
// Extended selector fam key will follow format of `[key]__selectorFamily/"chromogenDummyParam"/1`
const extendedKey = imported(dummyParam).key;
key = extendedKey.includes('selectorFamily')
? extendedKey.substring(0, extendedKey.indexOf('selectorFamily') - 2)
: extendedKey.substring(0, extendedKey.indexOf(`"${dummyParam}"`) - 2);
} else {
key = imported.key;
}
newStore.set(key, variable);
});
});
setStoreMap(newStore);
}
}, []);
useRecoilTransactionObserver_UNSTABLE(
({ previousSnapshot, snapshot }: { previousSnapshot: Snapshot; snapshot: Snapshot }): void => {
// Map current snapshot to array of atom states
// Can't directly check recording hook b/c TransactionObserver runs before state update
if (snapshot.getLoadable(recordingState).contents) {
const { transactions, setTransactions, atoms, atomFamilies } = ledger;
const state = atoms.map((item) => {
const { key } = item;
const value = snapshot.getLoadable(item).contents;
const previous = previousSnapshot.getLoadable(item).contents;
const updated = value !== previous;
return { key, value, previous, updated };
});
const atomFamilyState: AtomFamilyState[] = [];
/* eslint-disable */
// TODO: refactor out of for-in syntax b/c for-in tracks up the prototype chain x_x
for (const family in atomFamilies) {
const familyMembers = atomFamilies[family];
for (const member in familyMembers) {
const memberRecoilState = familyMembers[member];
let { key } = memberRecoilState;
/* Key will be auto-generated by recoil in the format of
* [atomFamilyName] + "__" + [params] + "__withFallback".
* Removing the "__withFallback" suffix to enhance readability
*/
key = key.substring(0, key.length - 14);
const value = snapshot.getLoadable(memberRecoilState).contents;
const previous = previousSnapshot.getLoadable(memberRecoilState).contents;
const updated = value !== previous;
// Don't track dummy atom generated by onload useEffect hook
if (!key.includes(dummyParam)) atomFamilyState.push({ family, key, value, updated });
}
}
/* eslint-enable */
transactions.push({ state, updates: [], atomFamilyState, familyUpdates: [] });
setTransactions.push({ state, setter: null });
}
},
);
const [pauseColor, setPauseColor] = useState('#90d1f0');
const pauseBorderStyle = {
borderColor: `${pauseColor}`,
};
const [playColor, setPlayColor] = useState('transparent transparent transparent #90d1f0')
const playBorderStyle = {
borderColor: `${playColor}`,
};
return (
<>
{
// Render button div only if DevTool not connected
!devtool && (
<div>
<div style={styles.divStyle}>
<button
aria-label={recording ? 'pause' : 'record'}
id="chromogen-toggle-record"
style={{ ...styles.buttonStyle, backgroundColor: '#7f7f7f' }}
type="button"
onClick={() => {
setRecording(!recording);
// if (!recording) return true;
// return false;
}}
onMouseEnter={() => recording ? setPauseColor('#f6f071') : setPlayColor('transparent transparent transparent #f6f071')}
onMouseLeave={() => recording ? setPauseColor('#90d1f0') : setPlayColor('transparent transparent transparent #90d1f0')}
><a>{recording ?
<div style={{ ...styles.pauseStyle, ...pauseBorderStyle }}></div>
: <div style={{ ...styles.playStyle, ...playBorderStyle }}></div>
}</a>
</button>
<button
aria-label="capture test"
id="chromogen-generate-file"
style={{ ...styles.buttonStyle, backgroundColor: '#7f7f7f', marginLeft: '-2px', marginRight: '13px' }}
type="button"
onClick={() => generateFile(setFile, storeMap)}
onMouseEnter={() => document.getElementById("chromogen-generate-file")!.style.color = '#f6f071'}
onMouseLeave={() => document.getElementById("chromogen-generate-file")!.style.color = '#90d1f0'}
><a>{'Download'}</a>
</button>
<button
aria-label="copy test"
id="chromogen-copy-test"
style={{ ...styles.buttonStyle, backgroundColor: '#7f7f7f', marginLeft: '-2px', marginRight: '13px' }}
type="button"
onClick={() => { navigator.clipboard.writeText(generateTests(storeMap)[0]) }}
onMouseEnter={() => document.getElementById("chromogen-copy-test")!.style.color = '#f6f071'}
onMouseLeave={() => document.getElementById("chromogen-copy-test")!.style.color = '#90d1f0'}
><a>{'Copy To Clipboard'}</a>
</button>
</div>
</div>
)
}
<a
download="chromogen.test.js"
href={file}
id="chromogen-download"
style={{ display: 'none' }}
>
Download Test
</a>
</>
);
};
================================================
FILE: package/recoil_generator/src/component/component-utils.ts
================================================
/* eslint-disable */
import type { CSSProperties } from 'react';
import type { SerializableParam } from 'recoil';
import type { Ledger } from '../types';
import { ledger } from '../utils/ledger';
import { convertFamilyTrackerKeys } from '../utils/utils';
import { output } from '../output/output';
/* eslint-enable */
const buttonStyle: CSSProperties = {
display: 'inline-block',
margin: '8px',
marginLeft: '13px',
padding: '0px',
height: '25px',
width: '65px',
borderRadius: '4px',
justifyContent: 'space-evenly',
border: '1px',
cursor: 'pointer',
color: '#90d1f0',
fontSize: '10px',
};
const divStyle: CSSProperties = {
display: 'flex',
position: 'absolute',
bottom: '100px',
left: '100px',
backgroundColor: '#aaa',
borderRadius: '4px',
margin: 0,
padding: 0,
zIndex: 999999,
};
const playStyle: CSSProperties = {
boxSizing: 'border-box',
marginLeft: '25px',
borderStyle: 'solid',
borderWidth: '7px 0px 7px 14px',
};
const pauseStyle: CSSProperties = {
width: '14px',
height: '14px',
borderWidth: '0px 0px 0px 10px',
borderStyle: 'double',
marginLeft: '27px',
};
export const styles = { buttonStyle, divStyle, playStyle, pauseStyle };
/**
* onclick function that generates test file & sets download URL
*
* Key-to-Variable name mapping is applied if storeMap has any contents
* (meaning atom / selector nodes were passed as props)
* Applying only at point-of-download keeps performance cost low for users who
* don't need to pass nodes while creating a moderate performance hit for others
* only while downloading, never while interacting with their app.
*/
export const generateTests = (storeMap: Map<string, string>): string[] => {
const {
atoms,
selectors,
setters,
atomFamilies,
selectorFamilies,
initialRender,
initialRenderFamilies,
transactions,
setTransactions,
} = ledger;
const finalLedger: Ledger<string, any, SerializableParam> =
storeMap.size > 0
? {
atoms: atoms.map(({ key }) => storeMap.get(key) || key),
selectors: selectors.map((key) => storeMap.get(key) || key),
atomFamilies: convertFamilyTrackerKeys(atomFamilies, storeMap),
selectorFamilies: convertFamilyTrackerKeys(selectorFamilies, storeMap),
setters: setters.map((key) => storeMap.get(key) || key),
initialRender: initialRender.map(({ key, value }) => {
const newKey = storeMap.get(key) || key;
return { key: newKey, value };
}),
initialRenderFamilies: initialRenderFamilies.map(({ key, value, params }) => {
const newKey = storeMap.get(key) || key;
return { key: newKey, value, params };
}),
transactions: transactions.map(({ state, updates, atomFamilyState, familyUpdates }) => {
const newState = state.map((eachAtom) => {
const key = storeMap.get(eachAtom.key) || eachAtom.key;
return { ...eachAtom, key };
});
const newUpdates = updates.map((eachSelector) => {
const key = storeMap.get(eachSelector.key) || eachSelector.key;
const { value } = eachSelector;
return { key, value };
});
const newAtomFamilyState = atomFamilyState.map((eachFamAtom) => {
const family = storeMap.get(eachFamAtom.family) || eachFamAtom.family;
const oldKey = eachFamAtom.key;
const keySuffix = oldKey.substring(eachFamAtom.family.length);
const key = family + keySuffix;
return { ...eachFamAtom, family, key };
});
const newFamilyUpdates = familyUpdates.map((eachFamSelector) => {
const key = storeMap.get(eachFamSelector.key) || eachFamSelector.key;
return { ...eachFamSelector, key };
});
return {
state: newState,
updates: newUpdates,
atomFamilyState: newAtomFamilyState,
familyUpdates: newFamilyUpdates,
};
}),
setTransactions: setTransactions.map(({ state, setter }) => {
const newState = state.map((eachAtom) => {
const key = storeMap.get(eachAtom.key) || eachAtom.key;
return { ...eachAtom, key };
});
const newSetter = setter;
if (newSetter) {
const { key } = newSetter;
newSetter.key = storeMap.get(key) || key;
}
return { state: newState, setter: newSetter };
}),
}
: { ...ledger, atoms: atoms.map(({ key }) => key) };
//return setFile(URL.createObjectURL(new Blob([output(finalLedger)])));
return [output(finalLedger)];
};
export const generateFile = (setFile: Function, storeMap: Map<string, string>): string[] => {
const tests = generateTests(storeMap);
const blob = new Blob(tests);
setFile(URL.createObjectURL(blob));
return tests;
};
================================================
FILE: package/recoil_generator/src/output/output-utils.ts
================================================
/* eslint-disable */
import type {
SelectorUpdate,
Transaction,
AtomUpdate,
SetTransaction,
AtomFamilies,
SelectorFamilies,
SelectorFamilyUpdate,
SelectorFamilyMembers,
} from '../types';
import { SerializableParam } from 'recoil';
/* eslint-enable */
/* ----- HELPER FUNCTIONS ----- */
export function initializeAtoms(state: AtomUpdate[], current: boolean): string {
return state.reduce(
(initializers, { key, value, previous }) =>
`${initializers}\t\t\tresult.current.set${key}(${JSON.stringify(
current ? value : previous,
)});\n\n`,
'',
);
}
//chromogen only captures selectors that fire; this pulls value from latest firing
export function assertState(updates: SelectorUpdate[]): string {
return updates.reduce(
(assertions, { key, value }) =>
`${assertions}\t\texpect(result.current.${key}Value).toStrictEqual(${JSON.stringify(
value,
)});\n\n`,
'',
);
}
/* ----- SETUP FUNCTIONS ----- */
export function importRecoilState(keyArray: string[]): string {
return keyArray.reduce((fullStr, key) => `${fullStr}\t${key},\n`, '');
}
export function importRecoilFamily(
familyObj: AtomFamilies | SelectorFamilies<any, SerializableParam>,
): string {
return Object.keys(familyObj).reduce(
(importStr, familyName) => `${importStr}\t${familyName},\n`,
'',
);
}
export function writeableHook(keyArray: string[]): string {
return keyArray.reduce(
(fullStr, key) => `${fullStr}\tconst [${key}Value, set${key}] = useRecoilState(${key});\n`,
'',
);
}
export function readableHook(keyArray: string[]): string {
return keyArray.reduce(
(fullStr, key) => `${fullStr}\tconst ${key}Value = useRecoilValue(${key});\n`,
'',
);
}
export function atomFamilyHook(transactionArray: Transaction[]): string {
const len = transactionArray.length;
return len
? transactionArray[len - 1].atomFamilyState.reduce((str, atomState) => {
const { family, key } = atomState;
/* Removing all special characters from string: if the params are passed
* in as a string, then we need to remove any special characters so they don't
* error out variable name generation, also recoil will add escaped quotes
* to the key name if it's a string, so will need to remove those by default
*/
const params = key.substring(family.length + 2);
const scrubbedParams = params.replace(/[^\w\s]/gi, '');
const parsedParams = JSON.parse(params);
return `${str}\tconst [${`${family}__${scrubbedParams}__Value`}, ${`set${family}__${scrubbedParams}`}] = useRecoilState(${family}(${
typeof parsedParams === 'string' ? `${params}` : `${parsedParams}`
}));\n`;
}, '')
: '';
}
export function selectorFamilyHook(
selectorFamilyTracker: SelectorFamilies<any, SerializableParam>,
isSettable: boolean,
): string {
return Object.entries(selectorFamilyTracker)
.filter((familyArr) => familyArr[1].isSettable === isSettable)
.reduce((str: string, familyArr: [string, { prevParams: Set<any> }]): string => {
const [familyName, { prevParams }] = familyArr;
// converting prevParams from set to array
return `${str}${[...prevParams].reduce((innerStr: string, param: any) => {
let scrubbedParams;
if (typeof param === 'string') {
scrubbedParams = param.replace(/[^\w\s]/gi, '');
}
return isSettable
? `${innerStr}\tconst [${`${familyName}__${
scrubbedParams !== undefined ? scrubbedParams : param
}__Value`}, ${`set${familyName}__${
scrubbedParams !== undefined ? scrubbedParams : param
}`}] = useRecoilState(${familyName}(${
typeof param === 'string' ? `"${param}"` : `${JSON.parse(param)}`
}));\n`
: `${innerStr}\tconst ${`${familyName}__${
scrubbedParams !== undefined ? scrubbedParams : param
}__Value`} = useRecoilValue(${familyName}(${
typeof param === 'string' ? `"${param}"` : `${JSON.parse(param)}`
}));\n`;
}, '')}`;
}, '');
}
export function returnWriteable(keyArray: string[]): string {
return keyArray.reduce((fullStr, key) => `${fullStr}\t\t${key}Value,\n\t\tset${key},\n`, '');
}
export function returnReadable(keyArray: string[]): string {
return keyArray.reduce((fullStr, key) => `${fullStr}\t\t${key}Value,\n`, '');
}
export function returnAtomFamily(transactionArray: Transaction[]): string {
const len = transactionArray.length;
return len
? transactionArray[len - 1].atomFamilyState.reduce((value, atomState) => {
const { family, key } = atomState;
// key will be "[familyname]__[params]"
const params = key.substring(family.length + 2);
const scrubbedParams = params.replace(/[^\w\s]/gi, '');
return `${value}\t\t${`${family}__${scrubbedParams}__Value`},
\t\t${`set${family}__${scrubbedParams}`},\n`;
}, '')
: '';
}
export function returnSelectorFamily(
selectorFamilyTracker: SelectorFamilies<any, SerializableParam>,
isSettable: boolean,
) {
return Object.entries(selectorFamilyTracker)
.filter((familyArr) => familyArr[1].isSettable === isSettable)
.reduce(
(str: string, familyArr: [string, SelectorFamilyMembers<any, SerializableParam>]): string => {
const [familyName, { prevParams }] = familyArr;
if (isSettable) {
return `${str}${[...prevParams].reduce((innerStr: string, param: any) => {
let scrubbedParams;
if (typeof param === 'string') {
scrubbedParams = param.replace(/[^\w\s]/gi, '');
}
return `${innerStr}\t\t${`${familyName}__${
scrubbedParams !== undefined ? scrubbedParams : param
}__Value`},
${`set${familyName}__${scrubbedParams !== undefined ? scrubbedParams : param}`},\n`;
}, '')}`;
}
return `${str}${[...prevParams].reduce((innerStr: string, param: any) => {
let scrubbedParams;
if (typeof param === 'string') {
scrubbedParams = param.replace(/[^\w\s]/gi, '');
}
return `${innerStr}\t\t${`${familyName}__${
scrubbedParams !== undefined ? scrubbedParams : param
}__Value`},\n`;
}, '')}`;
},
'',
);
}
/* ----- INITIAL RENDER ----- */
export function initializeSelectors(initialRender: SelectorUpdate[]): string {
return initialRender.reduce(
(fullStr, { key, value }) => `${fullStr}\tit('${key} should initialize correctly', () => {
\t\texpect(result.current.${key}Value).toStrictEqual(${JSON.stringify(value)});
\t});\n\n`,
'',
);
}
export function initializeSelectorFamilies(initialRenderFamilies: SelectorFamilyUpdate[]) {
return initialRenderFamilies.reduce((initialTests, { key, params, value }) => {
let scrubbedParams;
if (typeof params === 'string') {
scrubbedParams = params.replace(/[^\w\s]/gi, '');
}
return `${initialTests}\tit('${key}__${
scrubbedParams !== undefined ? scrubbedParams : JSON.stringify(params)
} should initialize correctly', () => {
\t\texpect(result.current.${key}__${
scrubbedParams !== undefined ? scrubbedParams : JSON.stringify(params)
}__Value).toStrictEqual(${JSON.stringify(value)});
\t});\n`;
}, '');
}
/* ----- SELECTORS TEST ----- */
//checking get methods
export function testSelectors(transactionArray: Transaction[]): string {
return transactionArray.reduce(
(selectorTests, { state, updates, atomFamilyState, familyUpdates }) => {
//checking to make sure chromogen doesn't look at atoms that haven't changed state
const allUpdatedAtoms = [
...state.filter(({ updated }) => updated),
...atomFamilyState.filter(({ updated }) => updated),
];
const allUpdatedSelectors: any[] = [...updates, ...familyUpdates];
const atomLen = allUpdatedAtoms.length;
const selectorLen = allUpdatedSelectors.length;
return atomLen !== 0 && selectorLen !== 0
? `${selectorTests}\tit('${
selectorLen > 1
? allUpdatedSelectors.reduce((list, selectorState, i) => {
const { key } = selectorState;
const isLastElement = i === selectorLen - 1;
// if params exist, then we are looking at a selectorFamily
if ('params' in selectorState) {
let scrubbedParams;
if (typeof selectorState.params === 'string') {
scrubbedParams = selectorState.params.replace(/[^\w\s]/gi, '');
}
return `${list}${isLastElement ? 'and ' : ''}${key}__${
scrubbedParams !== undefined ? scrubbedParams : selectorState.params
}${isLastElement ? '' : ', '}`;
}
return `${list}${isLastElement ? 'and ' : ''}${key}${isLastElement ? '' : ', '}`;
}, '')
: `${
allUpdatedSelectors[0].params !== undefined
? `${allUpdatedSelectors[0].key}__${
typeof allUpdatedSelectors[0].params === 'string'
? allUpdatedSelectors[0].params.replace(/[^\w\s]/gi, '')
: allUpdatedSelectors[0].params
}`
: allUpdatedSelectors[0].key
}`
} should properly derive state when ${
atomLen > 1
? allUpdatedAtoms.reduce((list, { key }, i) => {
const isLastElement = i === atomLen - 1;
const scrubbedKey = key.replace(/[^\w\s]/gi, '');
return `${list}${isLastElement ? 'and ' : ''}${scrubbedKey}${
isLastElement ? ' update' : ', '
}`;
}, '')
: `${allUpdatedAtoms[0].key.replace(/[^\w\s]/gi, '')} updates`
}', () => {
\t\tconst { result } = renderRecoilHook(useStoreHook);
\t\tact(() => {
${state.reduce(
(initializers, { key, value }) =>
`${initializers}\t\t\tresult.current.set${key}(${JSON.stringify(value)});\n\n`,
'',
)}
${atomFamilyState.reduce((initializers, { key, value }) => {
const scrubbedKey = key.replace(/[^\w\s]/gi, '');
return `${initializers}\t\t\tresult.current.set${scrubbedKey}(${JSON.stringify(value)});\n\n`;
}, '')}
\t\t});
${
selectorLen !== 0
? allUpdatedSelectors.reduce((assertions, selectorState) => {
const { key, value } = selectorState;
let scrubbedParams;
if (typeof selectorState.params === 'string') {
scrubbedParams = selectorState.params.replace(/[^\w\s]/gi, '');
}
if (selectorState.params !== undefined)
return `${assertions}\t\texpect(result.current.${key}__${
scrubbedParams !== undefined ? scrubbedParams : selectorState.params
}__Value).toStrictEqual(${JSON.stringify(value)});\n\n`;
return `${assertions}\t\texpect(result.current.${key}Value).toStrictEqual(${JSON.stringify(
value,
)});\n\n`;
}, '')
: ''
}\t});\n\n`
: selectorTests;
},
'',
);
}
/* ----- SETTERS TEST ----- */
export function testSetters(setTransactionArray: SetTransaction[]): string {
return setTransactionArray.reduce((setterTests, { state, setter }) => {
const updatedAtoms = state.filter(({ updated }) => updated);
if (setter) {
const { params } = setter;
let scrubbedParams;
if (typeof params === 'string') {
scrubbedParams = params.replace(/[^\w\s]/gi, '');
}
return params !== undefined
? `${setterTests}\tit('${setter.key}__${
scrubbedParams !== undefined ? scrubbedParams : JSON.stringify(params)
} should properly set state', () => {
\t\tconst { result } = renderRecoilHook(useStoreHook);
\t\tact(() => {
${initializeAtoms(state, false)}\t\t});
\t\tact(() => {
\t\t\tresult.current.set${setter.key}__${
scrubbedParams !== undefined ? scrubbedParams : JSON.stringify(params)
}(${JSON.stringify(setter.newValue)});
\t\t});
${assertState(updatedAtoms)}\t});\n\n`
: `${setterTests}\tit('${setter.key} should properly set state', () => {
\t\tconst { result } = renderRecoilHook(useStoreHook);
\t\tact(() => {
${initializeAtoms(state, false)}\t\t});
\t\tact(() => {
\t\t\tresult.current.set${setter.key}(${JSON.stringify(setter.newValue)});
\t\t});
${assertState(updatedAtoms)}\t});\n\n`;
}
return setterTests;
}, '');
}
================================================
FILE: package/recoil_generator/src/output/output.ts
================================================
/* eslint-disable */
import type { Ledger } from '../types';
import type { SerializableParam } from 'recoil';
import {
importRecoilState,
writeableHook,
readableHook,
returnWriteable,
returnReadable,
testSelectors,
testSetters,
importRecoilFamily,
atomFamilyHook,
selectorFamilyHook,
returnSelectorFamily,
initializeSelectors,
returnAtomFamily,
} from './output-utils';
/* eslint-enable */
/* ----- HELPERS ----- */
export const setFilter = (selectors: string[], setters: string[]): string[] =>
selectors.filter((key) => !setters.includes(key));
/* ----- MAIN ----- */
export const output = ({
atoms,
selectors,
setters,
atomFamilies,
selectorFamilies,
initialRender,
transactions,
setTransactions,
}: Ledger<string, any, SerializableParam>): string =>
`import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library';
import { useRecoilValue, useRecoilState } from 'recoil';
import {
${
importRecoilState(selectors)
+ importRecoilFamily(selectorFamilies)
}
} from '<ADD STORE FILEPATH>';
import {
${
importRecoilState(atoms)
+ importRecoilFamily(atomFamilies)
}
} from '<ADD ATOM FILEPATH>';
// 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
${writeableHook(atoms)}
// writeable selectors
${writeableHook(setters)}
// read-only selectors
${readableHook(setFilter(selectors, setters))}
// atom families
${atomFamilyHook(transactions)}
// writeable selector families
${selectorFamilyHook(selectorFamilies, true)}
// read-only selector families
${selectorFamilyHook(selectorFamilies, false)}
return {
${
returnWriteable(atoms)
+ returnWriteable(setters)
+ returnReadable(setFilter(selectors, setters))
+ returnAtomFamily(transactions)
+ returnSelectorFamily(selectorFamilies, true)
+ returnSelectorFamily(selectorFamilies, false)
}\t};
};
describe('INITIAL RENDER', () => {
const { result } = renderRecoilHook(useStoreHook);
${initializeSelectors(initialRender)}
});
describe('SELECTORS', () => {
${testSelectors(transactions)}});
describe('SETTERS', () => {
${testSetters(setTransactions)}});`;
================================================
FILE: package/recoil_generator/src/types.ts
================================================
/* eslint-disable */
import type {
RecoilState,
RecoilValue,
DefaultValue,
SerializableParam,
RecoilValueReadOnly,
} from 'recoil';
/* eslint-enable */
// ----- INITIALIZING NON-IMPORTABLE RECOIL TYPES -----
type ResetRecoilState = (recoilVal: RecoilState<any>) => void;
type GetRecoilValue = <T>(recoilVal: RecoilValue<T>) => T;
type SetRecoilState = <T>(
recoilVal: RecoilState<T>,
newVal: T | DefaultValue | ((prevValue: T) => T | DefaultValue),
) => void;
// ----- EXPORTING TYPES TO BE USED IN SRC/.TSX FILES -----
export interface SetterUpdate {
key: string;
newValue: any;
params?: SerializableParam;
}
export interface SelectorUpdate {
key: string;
value: any;
}
export interface SelectorFamilyUpdate extends SelectorUpdate {
params: SerializableParam;
}
export interface AtomUpdate extends SelectorUpdate {
previous: any;
updated: boolean;
}
export interface AtomFamilyState {
family: string;
key: string;
value: any;
updated: boolean;
}
export interface Transaction {
state: AtomUpdate[];
updates: SelectorUpdate[];
atomFamilyState: AtomFamilyState[];
familyUpdates: SelectorFamilyUpdate[];
}
export interface SetTransaction {
state: AtomUpdate[];
setter: null | SetterUpdate;
}
export interface AtomFamilyMembers {
[atomName: string]: RecoilState<any>;
}
export interface AtomFamilies {
[familyName: string]: AtomFamilyMembers;
}
export interface SelectorFamilyConfig<T, P extends SerializableParam> {
key: string;
get: (param: P) => (opts: { get: GetRecoilValue }) => Promise<T> | RecoilValue<T> | T;
set?: (
param: P,
) => (
opts: { set: SetRecoilState; get: GetRecoilValue; reset: ResetRecoilState },
newValue: T | DefaultValue,
) => void;
dangerouslyAllowMutability?: boolean;
}
export interface SelectorFamilyMembers<T, P> {
trackedSelectorFamily: (param: P) => RecoilState<T> | RecoilValueReadOnly<T>;
isSettable: boolean;
prevParams: Set<any>;
}
export interface SelectorFamilies<T, P> {
[familyName: string]: SelectorFamilyMembers<T, P>;
}
// atoms should take RecoilState<any>[] | string[]
export interface Ledger<T, S, P> {
atoms: T[];
selectors: string[];
atomFamilies: AtomFamilies;
selectorFamilies: SelectorFamilies<S, P>;
setters: string[];
initialRender: SelectorUpdate[];
initialRenderFamilies: SelectorFamilyUpdate[];
transactions: Transaction[];
setTransactions: SetTransaction[];
}
export interface SelectorConfig<T> {
key: string;
get: (opts: { get: GetRecoilValue }) => T | Promise<T> | RecoilValue<T>;
set?: (
opts: { get: GetRecoilValue; set: SetRecoilState; reset: ResetRecoilState },
newValue: T | DefaultValue,
) => void;
dangerouslyAllowMutability?: boolean;
}
================================================
FILE: package/recoil_generator/src/utils/ledger.ts
================================================
/* eslint-disable */
import type { Ledger } from '../types';
import { RecoilState, SerializableParam } from 'recoil';
/* eslint-enable */
export const ledger: Ledger<RecoilState<any>, any, SerializableParam> = {
atoms: [],
selectors: [], //get
atomFamilies: {},
selectorFamilies: {},
setters: [], //set
initialRender: [],
initialRenderFamilies: [],
transactions: [],//get
setTransactions: [],//set
};
================================================
FILE: package/recoil_generator/src/utils/store.ts
================================================
/* eslint-disable */
import type { RecoilState } from 'recoil';
import { atom } from 'recoil';
/* eslint-enable */
// Recording toggle
export const recordingState: RecoilState<boolean> = atom<boolean>({
key: 'recordingState',
default: true,
});
================================================
FILE: package/recoil_generator/src/utils/utils.ts
================================================
/* eslint-disable */
import type { SerializableParam } from 'recoil';
import type { AtomFamilies, SelectorFamilies } from '../types';
/* eslint-enable */
// Debouncing for selector transaction updates
export const debounce = (func: (...args: any[]) => any, wait: number) => {
let timeout: any;
return (...args: any[]) => {
const timeoutCallback = () => {
timeout = null;
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(timeoutCallback, wait);
};
};
// Used in key-to-variable name mapping in generateFile
export function convertFamilyTrackerKeys(
familyTracker: AtomFamilies,
storeMap: Map<string, string>,
): AtomFamilies;
export function convertFamilyTrackerKeys<T, P extends SerializableParam>(
familyTracker: SelectorFamilies<T, P>,
storeMap: Map<string, string>,
): SelectorFamilies<T, P>;
export function convertFamilyTrackerKeys(
familyTracker: AtomFamilies | SelectorFamilies<any, SerializableParam>,
storeMap: Map<string, string>,
) {
const refactoredTracker: AtomFamilies | SelectorFamilies<any, SerializableParam> = {};
Object.keys(familyTracker).forEach((key) => {
const newKey: string = storeMap.get(key) || key;
refactoredTracker[newKey] = familyTracker[key];
});
return refactoredTracker;
}
// Dummy param for use in various checks (most notably the key-to-variable name mapping)
export const dummyParam = 'chromogenDummyParam';
================================================
FILE: package/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
"declaration": true /* Generates corresponding '.d.ts' file. */,
"outDir": "./build" /* Redirect output structure to the directory. */,
"rootDir": "." /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"removeComments": true /* Do not emit comments to output. */,
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
// "include": ["package/**/*"]
}
================================================
FILE: package/zustand_generator/__tests__/api.test.js
================================================
import { ledger } from '../src/utils/ledger';
import { chromogenZustandMiddleware } from '../src/api/api';
import { renderHook, act } from '@testing-library/react';
import create from 'zustand';
// testing chromogenZustandMiddleware
describe('chromogenZustandMiddleware', () => {
// destructuring atoms from ledger interface in utils folder
it('is a function', () => {
expect(typeof chromogenZustandMiddleware).toBe('function');
});
it('should update ledger upon invocation', () => {
// creating a mock store
const useStore = create(
chromogenZustandMiddleware((set) => ({
count: 0,
increment: () => {
set((state) => ({ count: count + 1 }), false, 'increment');
},
})),
);
//rendering the useStore Hook
const { result } = renderHook(useStore);
// verifying atoms property (array) on ledger has been updated with input atom
expect(result.current.count).toStrictEqual(0);
expect(ledger.initialRender).toStrictEqual({ count: 0 });
});
});
================================================
FILE: package/zustand_generator/__tests__/output-utils.test.js
================================================
import {
testInitialState,
testStateChangesExpect,
testStateChangesAct,
generateActLine
} from '../src/output/output-utils';
const initialRender = {
todoListState: [],
todoListFilterState: 'Show All',
todoListSortState: false,
quoteText: '',
quoteNumber: 0,
checkBox: false,
}
describe('INITIAL RENDER', () => {
//create a variable to hold our expected output
const expectedOutput = ''
+ `\tit('todoListState should initialize correctly', () => {\n\t\texpect(result.current.todoListState).toStrictEqual([]);\n\t});\n\n`
+ `\tit('todoListFilterState should initialize correctly', () => {\n\t\texpect(result.current.todoListFilterState).toStrictEqual("Show All");\n\t});\n\n`
+ `\tit('todoListSortState should initialize correctly', () => {\n\t\texpect(result.current.todoListSortState).toStrictEqual(false);\n\t});\n\n`
+ `\tit('quoteText should initialize correctly', () => {\n\t\texpect(result.current.quoteText).toStrictEqual("");\n\t});\n\n`
+ `\tit('quoteNumber should initialize correctly', () => {\n\t\texpect(result.current.quoteNumber).toStrictEqual(0);\n\t});\n\n`
+ `\tit('checkBox should initialize correctly', () => {\n\t\texpect(result.current.checkBox).toStrictEqual(false);\n\t});\n\n`
//create a variable and assign it to the evaluated result of the calling the testInitialState on our input
const evaluatedResult = testInitialState(initialRender);
//expect(realOutput).toStrictEqual(expectedOutput);
it('expectedOutput should equal evaulatedResult', () => {
expect(evaluatedResult).toStrictEqual(expectedOutput);
});
})
describe('TEST STATE CHANGES ACT', () => {
const transaction = [
{
action: 'setFilter',
arguments: ['Show Uncompleted'],
changedValues: { 'todoListFilterState': 'Show Uncompleted' }
}
];
const testStateChangesActOutput = testStateChangesAct(transaction);
const expectedOutput =
`\n\tit('todoListFilterState should update correctly', () => {
const { result } = renderHook(useStore);
act(() => {\n result.current.setFilter("Show Uncompleted");\n});
\nexpect(result.current.todoListFilterState).toStrictEqual("Show Uncompleted");
});`;
let expectedNoWhitespace = expectedOutput.replace(/\s/g, '');
it('expect output to equal expected output', () => {
expect(testStateChangesActOutput.replace(/\s/g, '')).toStrictEqual(expectedNoWhitespace)
})
})
describe('TEST STATE CHANGES EXPECT', () => {
//1. Create function inputs manually
const input = ["todoListFilterState", "Show Completed"];
//2. Manually write out expected output of function
const expectedOutput = `\nexpect(result.current.todoListFilterState).toStrictEqual("Show Completed");`
//3. Run function to get actual output
//4. Compare exptected ouptut to actual output
const testStateChanges = testStateChangesExpect(input)
it('it should be true when when input is passed into testStateChanges function ', () => {
expect(testStateChanges).toStrictEqual(expectedOutput);
})
})
describe('GENERATE ACT LINE', () => {
// Create a tranaction inputs manually
const transaction = {
action: 'setFilter',
arguments: ['Show Completed'],
changedValues: { todoListFilterState: "Show Completed" }
};
const action = transaction.action;
const args = transaction.arguments;
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
SYMBOL INDEX (68 symbols across 16 files)
FILE: demo-todo/src/components/TodoItem.jsx
function replaceItemAtIndex (line 7) | function replaceItemAtIndex(arr, index, newValue) {
function removeItemAtIndex (line 11) | function removeItemAtIndex(arr, index) {
FILE: package/recoil_generator/src/api/api.ts
function selector (line 46) | function selector(config: ReadWriteSelectorOptions<any> | ReadOnlySelect...
function atom (line 76) | function atom<T>(config: AtomOptions<T>): RecoilState<T> {
function atomFamily (line 87) | function atomFamily<T, P extends SerializableParam>(
function selectorFamily (line 118) | function selectorFamily<T>(
FILE: package/recoil_generator/src/api/core-utils.ts
constant DEBOUNCE_MS (line 9) | const DEBOUNCE_MS = 250;
FILE: package/recoil_generator/src/output/output-utils.ts
function initializeAtoms (line 17) | function initializeAtoms(state: AtomUpdate[], current: boolean): string {
function assertState (line 27) | function assertState(updates: SelectorUpdate[]): string {
function importRecoilState (line 39) | function importRecoilState(keyArray: string[]): string {
function importRecoilFamily (line 43) | function importRecoilFamily(
function writeableHook (line 52) | function writeableHook(keyArray: string[]): string {
function readableHook (line 59) | function readableHook(keyArray: string[]): string {
function atomFamilyHook (line 66) | function atomFamilyHook(transactionArray: Transaction[]): string {
function selectorFamilyHook (line 88) | function selectorFamilyHook(
function returnWriteable (line 120) | function returnWriteable(keyArray: string[]): string {
function returnReadable (line 124) | function returnReadable(keyArray: string[]): string {
function returnAtomFamily (line 128) | function returnAtomFamily(transactionArray: Transaction[]): string {
function returnSelectorFamily (line 142) | function returnSelectorFamily(
function initializeSelectors (line 180) | function initializeSelectors(initialRender: SelectorUpdate[]): string {
function initializeSelectorFamilies (line 189) | function initializeSelectorFamilies(initialRenderFamilies: SelectorFamil...
function testSelectors (line 208) | function testSelectors(transactionArray: Transaction[]): string {
function testSetters (line 300) | function testSetters(setTransactionArray: SetTransaction[]): string {
FILE: package/recoil_generator/src/types.ts
type ResetRecoilState (line 12) | type ResetRecoilState = (recoilVal: RecoilState<any>) => void;
type GetRecoilValue (line 14) | type GetRecoilValue = <T>(recoilVal: RecoilValue<T>) => T;
type SetRecoilState (line 16) | type SetRecoilState = <T>(
type SetterUpdate (line 22) | interface SetterUpdate {
type SelectorUpdate (line 28) | interface SelectorUpdate {
type SelectorFamilyUpdate (line 33) | interface SelectorFamilyUpdate extends SelectorUpdate {
type AtomUpdate (line 37) | interface AtomUpdate extends SelectorUpdate {
type AtomFamilyState (line 42) | interface AtomFamilyState {
type Transaction (line 49) | interface Transaction {
type SetTransaction (line 56) | interface SetTransaction {
type AtomFamilyMembers (line 61) | interface AtomFamilyMembers {
type AtomFamilies (line 64) | interface AtomFamilies {
type SelectorFamilyConfig (line 68) | interface SelectorFamilyConfig<T, P extends SerializableParam> {
type SelectorFamilyMembers (line 79) | interface SelectorFamilyMembers<T, P> {
type SelectorFamilies (line 84) | interface SelectorFamilies<T, P> {
type Ledger (line 89) | interface Ledger<T, S, P> {
type SelectorConfig (line 101) | interface SelectorConfig<T> {
FILE: package/recoil_generator/src/utils/utils.ts
function convertFamilyTrackerKeys (line 31) | function convertFamilyTrackerKeys(
FILE: package/zustand_generator/src/api/api.ts
type Chromogen (line 8) | type Chromogen = <
type ChromogenImpl (line 16) | type ChromogenImpl = <T extends unknown>(
type PopArgument (line 20) | type PopArgument<T extends (...a: never[]) => unknown> = T extends (
type TakeTwo (line 26) | type TakeTwo<T> = T extends []
type StoreDevtools (line 46) | type StoreDevtools<S> = S extends {
type Write (line 54) | type Write<T, U> = Omit<T, keyof U> & U;
type WithDevtools (line 56) | type WithDevtools<S> = Write<S, StoreDevtools<S>>;
type NamedSet (line 58) | type NamedSet<T> = WithDevtools<StoreApi<T>>['setState'];
type S (line 70) | type S = ReturnType<typeof creatorFunction>;
FILE: package/zustand_generator/src/component/Buttons/SecondaryButton.tsx
type Icon (line 72) | type Icon = 'download' | 'expand' | 'retract' | 'copy' | 'arrow' | 'check';
type Props (line 74) | interface Props {
FILE: package/zustand_generator/src/component/ChromogenZustandObserver.tsx
type Props (line 13) | interface Props {
FILE: package/zustand_generator/src/component/Editor.tsx
type Props (line 23) | interface Props {
FILE: package/zustand_generator/src/component/EditorTab.tsx
type Props (line 4) | type Props = {
FILE: package/zustand_generator/src/component/Header.tsx
function copyOff (line 75) | function copyOff() {
FILE: package/zustand_generator/src/component/Resizing/Resizer.tsx
type ResizerProps (line 17) | interface ResizerProps {
FILE: package/zustand_generator/src/output/output-utils.ts
function importZustandStore (line 11) | function importZustandStore(): string {
function testInitialState (line 15) | function testInitialState(initialRender: InitialRender): string {
function testStateChangesAct (line 29) | function testStateChangesAct(transactions: Transaction<any>[]): string {
function testStateChangesExpect (line 72) | function testStateChangesExpect([propertyName, newValue]: [string, any])...
function generateActLine (line 77) | function generateActLine<T extends any[]>(t: Transaction<T>): string {
function generateItBlock (line 85) | function generateItBlock(transactions: Transaction<any>[]): { str: strin...
FILE: package/zustand_generator/src/types.ts
type NotAFunction (line 3) | type NotAFunction = { [k: string]: unknown } & ({ bind?: never } | { cal...
type InitialRender (line 5) | type InitialRender = {
type Transaction (line 9) | interface Transaction<T extends any[]> {
type Ledger (line 17) | interface Ledger {
FILE: package/zustand_generator/src/utils/store.ts
type RecordingState (line 3) | interface RecordingState {
Condensed preview — 103 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (252K chars).
[
{
"path": ".eslintrc.json",
"chars": 3001,
"preview": "{\n \"root\": true,\n \"parser\": \"@typescript-eslint/parser\",\n \"env\": {\n \"browser\": true,\n \"es2020\": true,\n "
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 834,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/dependabot.yml",
"chars": 114,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"npm\"\n directory: \"/package\"\n schedule:\n interval: \"weekly\"\n"
},
{
"path": ".github/pull_request_template.md",
"chars": 1050,
"preview": "## Types of changes\n<!--- What types of changes does your code introduce to Scratch Project? Put an `x` in the boxes tha"
},
{
"path": ".gitignore",
"chars": 151,
"preview": "# npm\nnode_modules\n\n# System files\n.DS_Store\n.vscode\n\n# Tests\ncoverage\n\n# Package\npackage/build\n\n# Demo App\nchromogen.te"
},
{
"path": ".prettierrc.json",
"chars": 285,
"preview": "{\n\t\"printWidth\": 100,\n \"tabWidth\": 2,\n \"useTabs\": false,\n \"semi\": true,\n \"singleQuote\": true,\n \"quoteProps\": \"as-ne"
},
{
"path": ".travis.yml",
"chars": 165,
"preview": "language: node_js\nnode_js:\n - 14\nenv:\n - TEST_DIR=package\nbefore_install:\n - cd $TEST_DIR\ninstall:\n - npm install\nsc"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3355,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "Dockerfile",
"chars": 557,
"preview": "FROM jenkins/jenkins:2.375.3\nUSER root\nRUN apt-get update && apt-get install -y lsb-release\nRUN curl -fsSLo /usr/share/k"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2020 OSLabs Beta\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 24721,
"preview": "<div align=\"center\">\n\n<a href=\"https://chromogen-site-eight.vercel.app/\">\n <img\n height=\"200\"\n width=\"450\"\n al"
},
{
"path": "demo-todo/.babelrc",
"chars": 128,
"preview": "{ \n \"presets\": [\n [\"@babel/preset-env\"],\n \"@babel/preset-react\"\n ],\n \"plugins\": [\n //\"react-hot-loader/babel"
},
{
"path": "demo-todo/LICENSE",
"chars": 1073,
"preview": "MIT License\n\nCopyright (c) 2020 Michelle Holland\n\nPermission is hereby granted, free of charge, to any person obtaining "
},
{
"path": "demo-todo/README.md",
"chars": 603,
"preview": "<div align=\"center\">\n\n# The official demo app for [Chromogen](https://github.com/oslabs-beta/Chromogen).\n\n![demo app int"
},
{
"path": "demo-todo/__tests__/initialTestTest.js",
"chars": 5135,
"preview": "import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library';\nimport { useRecoilValue, useRecoilState } fr"
},
{
"path": "demo-todo/package.json",
"chars": 1950,
"preview": "{\n \"name\": \"chromogen-todo\",\n \"version\": \"1.0.0\",\n \"description\": \"demo todo app for Chromogen using React + Recoil\","
},
{
"path": "demo-todo/src/components/App.jsx",
"chars": 378,
"preview": "import React from 'react';\nimport { RecoilRoot } from 'recoil';\nimport { ChromogenObserver } from 'chromogen';\nimport To"
},
{
"path": "demo-todo/src/components/Quotes.jsx",
"chars": 601,
"preview": "import React from 'react';\nimport { useRecoilValue, useSetRecoilState } from 'recoil';\nimport { quoteTextState, xkcdStat"
},
{
"path": "demo-todo/src/components/ReadOnlyTodoItem.jsx",
"chars": 900,
"preview": "import React from 'react';\nimport Checkbox from '@mui/material/Checkbox';\nimport '../styles/styles.css';\nimport { todoLi"
},
{
"path": "demo-todo/src/components/SearchBar.jsx",
"chars": 1434,
"preview": "import React, { useState } from 'react';\nimport { useRecoilState } from 'recoil';\nimport { searchBarSelectorFam } from '"
},
{
"path": "demo-todo/src/components/TodoItem.jsx",
"chars": 1683,
"preview": "import React from 'react';\nimport { useRecoilState } from 'recoil';\nimport Checkbox from '@mui/material/Checkbox';\nimpor"
},
{
"path": "demo-todo/src/components/TodoItemCreator.jsx",
"chars": 2429,
"preview": "/* eslint-disable react/jsx-props-no-spreading */\nimport React, { useState } from 'react';\nimport RadioGroup from '@mui/"
},
{
"path": "demo-todo/src/components/TodoList.jsx",
"chars": 1136,
"preview": "import React from 'react';\nimport { useRecoilValue } from 'recoil';\nimport { sortedTodoListState } from '../store/store'"
},
{
"path": "demo-todo/src/components/TodoListFilters.jsx",
"chars": 3060,
"preview": "import React, { useState } from 'react';\nimport SortIcon from '@mui/icons-material/Sort';\nimport EqualizerIcon from '@mu"
},
{
"path": "demo-todo/src/components/TodoQuickCheck.jsx",
"chars": 729,
"preview": "import React from 'react';\nimport { useRecoilState, useRecoilValue } from 'recoil';\nimport Checkbox from '@mui/material/"
},
{
"path": "demo-todo/src/index.html",
"chars": 488,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "demo-todo/src/index.js",
"chars": 284,
"preview": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react';\n//import { render } from 'react-dom';\nimpor"
},
{
"path": "demo-todo/src/store/atoms.js",
"chars": 1049,
"preview": "import { atom } from 'chromogen';\n\n/* ----- ATOMS ----- */\n\n// unsorted, unfiltered todo list\nconst todoListState = atom"
},
{
"path": "demo-todo/src/store/store.js",
"chars": 5083,
"preview": "import { selector, selectorFamily } from 'chromogen';\nimport {\n todoListState,\n todoListFilterState,\n todoListSortSta"
},
{
"path": "demo-todo/src/styles/styles.css",
"chars": 4093,
"preview": "/* -------------General Styles---------------- */\nhtml {\n margin: 0;\n background-color: rgb(48, 48, 48);\n color: whit"
},
{
"path": "demo-todo/webpack.config.js",
"chars": 810,
"preview": "const path = require('path');\n\nmodule.exports = {\n entry: path.resolve(__dirname, './src/index.js'),\n output: {\n fi"
},
{
"path": "demo-zustand-todo/.babelrc",
"chars": 128,
"preview": "{ \n \"presets\": [\n [\"@babel/preset-env\"],\n \"@babel/preset-react\"\n ],\n \"plugins\": [\n //\"react-hot-loader/babel"
},
{
"path": "demo-zustand-todo/LICENSE",
"chars": 1073,
"preview": "MIT License\n\nCopyright (c) 2020 Michelle Holland\n\nPermission is hereby granted, free of charge, to any person obtaining "
},
{
"path": "demo-zustand-todo/__tests__/sampleTest.js",
"chars": 6115,
"preview": "import { renderHook, act } from '@testing-library/react';\nimport useStore from '../src/store/store';\n\ndescribe('INITIAL "
},
{
"path": "demo-zustand-todo/package.json",
"chars": 2683,
"preview": "{\n \"name\": \"chromogen-todo\",\n \"version\": \"1.0.2\",\n \"description\": \"demo todo app for Chromogen using React + Recoil\","
},
{
"path": "demo-zustand-todo/src/components/App.jsx",
"chars": 285,
"preview": "import React from 'react';\nimport { ChromogenZustandObserver } from 'chromogen';\nimport TodoList from './TodoList';\nimpo"
},
{
"path": "demo-zustand-todo/src/components/Quotes.jsx",
"chars": 987,
"preview": "import React from 'react';\nimport shallow from 'zustand/shallow';\nimport useToDoStore from '../store/store';\nimport { us"
},
{
"path": "demo-zustand-todo/src/components/ReadOnlyTodoItem.jsx",
"chars": 869,
"preview": "import React from 'react';\nimport Checkbox from '@mui/material/Checkbox';\nimport '../styles/styles.css';\nimport useToDoS"
},
{
"path": "demo-zustand-todo/src/components/SearchBar.jsx",
"chars": 1674,
"preview": "import React, { useState } from 'react';\nimport useToDoStore from '../store/store';\nimport shallow from 'zustand/shallow"
},
{
"path": "demo-zustand-todo/src/components/TodoItem.jsx",
"chars": 1250,
"preview": "import React from 'react';\nimport Checkbox from '@mui/material/Checkbox';\nimport '../styles/styles.css';\nimport shallow "
},
{
"path": "demo-zustand-todo/src/components/TodoItemCreator.jsx",
"chars": 2366,
"preview": "/* eslint-disable react/jsx-props-no-spreading */\nimport React, { useState } from 'react';\nimport RadioGroup from '@mui/"
},
{
"path": "demo-zustand-todo/src/components/TodoList.jsx",
"chars": 2288,
"preview": "import React from 'react';\nimport TodoItem from './TodoItem';\nimport TodoItemCreator from './TodoItemCreator';\nimport To"
},
{
"path": "demo-zustand-todo/src/components/TodoListFilters.jsx",
"chars": 3307,
"preview": "import React, { useState } from 'react';\nimport SortIcon from '@mui/icons-material/Sort';\nimport EqualizerIcon from '@mu"
},
{
"path": "demo-zustand-todo/src/components/TodoQuickCheck.jsx",
"chars": 777,
"preview": "import React from 'react';\nimport Checkbox from '@mui/material/Checkbox';\nimport shallow from 'zustand/shallow';\nimport "
},
{
"path": "demo-zustand-todo/src/index.html",
"chars": 496,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "demo-zustand-todo/src/index.js",
"chars": 241,
"preview": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react';\nimport App from './components/App';\nimport "
},
{
"path": "demo-zustand-todo/src/store/store.js",
"chars": 3811,
"preview": "import { chromogenZustandMiddleware } from 'chromogen';\nimport { create } from 'zustand';\n\nconst useToDoStore = create(\n"
},
{
"path": "demo-zustand-todo/src/styles/styles.css",
"chars": 6355,
"preview": "*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n* {\n margin: 0;\n}\nhtml,\nbody,\n#root, /* for create-react-app */\n#_"
},
{
"path": "demo-zustand-todo/webpack.config.js",
"chars": 912,
"preview": "const path = require('path');\n\nmodule.exports = {\n entry: path.resolve(__dirname, './src/index.js'),\n output: {\n fi"
},
{
"path": "jenkins/Jenkinsfile",
"chars": 690,
"preview": "pipeline {\n agent {\n docker {\n image 'node:lts-buster-slim'\n args '-p 3003:3003'\n "
},
{
"path": "jenkins/scripts/deliver.sh",
"chars": 1267,
"preview": "#!/usr/bin/env sh\n\necho 'The following \"npm\" command builds your Node.js/React application for'\necho 'production in the "
},
{
"path": "jenkins/scripts/kill.sh",
"chars": 224,
"preview": "#!/usr/bin/env sh\n\necho 'The following command terminates the \"npm start\" process using its PID'\necho '(written to \".pid"
},
{
"path": "jenkins/scripts/test.sh",
"chars": 1044,
"preview": "#!/usr/bin/env sh\n\necho 'The following \"npm\" command (if executed) installs the \"cross-env\"'\necho 'dependency into the l"
},
{
"path": "package/LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2020 OSLabs Beta\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "package/README.md",
"chars": 7538,
"preview": "<div align=\"center\">\n<h1>Chromogen</h1>\n<a href=\"https://github.com/open-source-labs/Chromogen\">\n <img\n height=\"120\""
},
{
"path": "package/babel.config.js",
"chars": 160,
"preview": "module.exports = {\n presets: [\n ['@babel/preset-env', { targets: { node: 'current' } }],\n '@babel/preset-react',\n"
},
{
"path": "package/index.ts",
"chars": 648,
"preview": "/* eslint-disable */\nimport { atom, selector, atomFamily, selectorFamily } from './recoil_generator/src/api/api';\nimport"
},
{
"path": "package/package.json",
"chars": 3775,
"preview": "{\n \"name\": \"chromogen\",\n \"version\": \"5.0.1\",\n \"description\": \"simple, interaction-driven Jest test generator for Reco"
},
{
"path": "package/recoil_generator/__tests__/api.test.js",
"chars": 2857,
"preview": "import { ledger } from '../src/utils/ledger.ts';\nimport { atom, selector, selectorFamily, atomFamily } from '../src/api/"
},
{
"path": "package/recoil_generator/__tests__/component-utils.test.js",
"chars": 628,
"preview": "import { ledger } from '../src/utils/ledger.ts';\nimport {generateFile} from '../src/component/component-utils';\n\n// Test"
},
{
"path": "package/recoil_generator/__tests__/component.test.js",
"chars": 3374,
"preview": "import React from 'react';\nimport { RecoilRoot, useRecoilState } from 'recoil';\nimport { render } from '@testing-library"
},
{
"path": "package/recoil_generator/__tests__/core-utils.test.jx",
"chars": 275,
"preview": "import {\n debouncedAddToTransactions,\n wrapGetter,\n wrapSetter,\n} from '../src/api/core-utils';\n\nimport { debounce } "
},
{
"path": "package/recoil_generator/__tests__/output-utils.test.js",
"chars": 6509,
"preview": "import {\n initializeAtoms,\n assertState,\n testSelectors,\n testSetters,\n importRecoilFamily,\n atomFamilyHook,\n //w"
},
{
"path": "package/recoil_generator/__tests__/output.test.js",
"chars": 1141,
"preview": "import { setFilter, output } from '../src/output/output.ts';\n\n// testing setFilter function\ndescribe('setFilter', () => "
},
{
"path": "package/recoil_generator/__tests__/utils.test.js",
"chars": 2322,
"preview": "import { debounce, convertFamilyTrackerKeys } from '../src/utils/utils.ts';\n\njest.useFakeTimers();\n\ndescribe('debounce',"
},
{
"path": "package/recoil_generator/src/api/api.ts",
"chars": 5530,
"preview": "/* eslint-disable */\nimport type {\n RecoilState,\n RecoilValueReadOnly,\n AtomOptions,\n ReadWriteSelectorOptions,\n Re"
},
{
"path": "package/recoil_generator/src/api/core-utils.ts",
"chars": 2664,
"preview": "/* eslint-disable */\nimport { debounce } from '../utils/utils';\nimport { ledger } from '../utils/ledger';\nimport { recor"
},
{
"path": "package/recoil_generator/src/api/family-utils.ts",
"chars": 2266,
"preview": "/* eslint-disable */\nimport type { SerializableParam } from 'recoil';\n\nimport { ledger } from '../utils/ledger';\nimport "
},
{
"path": "package/recoil_generator/src/component/ChromogenObserver.tsx",
"chars": 8497,
"preview": "/* eslint-disable */\nimport type { Snapshot } from 'recoil';\nimport type { AtomFamilyState } from '../types';\n\nimport Re"
},
{
"path": "package/recoil_generator/src/component/component-utils.ts",
"chars": 5015,
"preview": "/* eslint-disable */\nimport type { CSSProperties } from 'react';\nimport type { SerializableParam } from 'recoil';\nimport"
},
{
"path": "package/recoil_generator/src/output/output-utils.ts",
"chars": 12868,
"preview": "/* eslint-disable */\nimport type {\n SelectorUpdate,\n Transaction,\n AtomUpdate,\n SetTransaction,\n AtomFamilies,\n Se"
},
{
"path": "package/recoil_generator/src/output/output.ts",
"chars": 2274,
"preview": "/* eslint-disable */\nimport type { Ledger } from '../types';\nimport type { SerializableParam } from 'recoil';\nimport {\n "
},
{
"path": "package/recoil_generator/src/types.ts",
"chars": 2736,
"preview": "/* eslint-disable */\nimport type {\n RecoilState,\n RecoilValue,\n DefaultValue,\n SerializableParam,\n RecoilValueReadO"
},
{
"path": "package/recoil_generator/src/utils/ledger.ts",
"chars": 420,
"preview": "/* eslint-disable */\nimport type { Ledger } from '../types';\nimport { RecoilState, SerializableParam } from 'recoil';\n/*"
},
{
"path": "package/recoil_generator/src/utils/store.ts",
"chars": 250,
"preview": "/* eslint-disable */\nimport type { RecoilState } from 'recoil';\nimport { atom } from 'recoil';\n/* eslint-enable */\n\n// R"
},
{
"path": "package/recoil_generator/src/utils/utils.ts",
"chars": 1428,
"preview": "/* eslint-disable */\nimport type { SerializableParam } from 'recoil';\nimport type { AtomFamilies, SelectorFamilies } fro"
},
{
"path": "package/tsconfig.json",
"chars": 2274,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES6\" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES"
},
{
"path": "package/zustand_generator/__tests__/api.test.js",
"chars": 1031,
"preview": "import { ledger } from '../src/utils/ledger';\nimport { chromogenZustandMiddleware } from '../src/api/api';\nimport { rend"
},
{
"path": "package/zustand_generator/__tests__/output-utils.test.js",
"chars": 3888,
"preview": "import {\n testInitialState,\n testStateChangesExpect,\n testStateChangesAct,\n generateActLine\n} from '../src/output/ou"
},
{
"path": "package/zustand_generator/src/GlobalStyle.ts",
"chars": 1971,
"preview": "/* Reset CSS section */\nimport { createGlobalStyle } from 'styled-components';\n\nconst GlobalStyle = createGlobalStyle`\n*"
},
{
"path": "package/zustand_generator/src/api/api.ts",
"chars": 3712,
"preview": "import { ledger } from '../utils/ledger';\nimport { Transaction, InitialRender } from '../types';\nimport { StoreApi, Stat"
},
{
"path": "package/zustand_generator/src/component/Buttons/RecordingButton.tsx",
"chars": 684,
"preview": "import React, { useState } from 'react';\nimport RecordButton from './RecordingVariations/Record';\nimport StartButton fro"
},
{
"path": "package/zustand_generator/src/component/Buttons/RecordingVariations/Record.tsx",
"chars": 3571,
"preview": "import React, { useState } from 'react';\n\nconst Record = (props): JSX.Element => {\n //hover\n const [isHover, setIsHove"
},
{
"path": "package/zustand_generator/src/component/Buttons/RecordingVariations/Start.tsx",
"chars": 1967,
"preview": "import React, { useState } from 'react';\n\nconst Start = (props): JSX.Element => {\n //hover\n const [isHover, setIsHover"
},
{
"path": "package/zustand_generator/src/component/Buttons/SecondaryButton.tsx",
"chars": 3171,
"preview": "import React, { useState } from 'react';\nimport {\n icon_arrow,\n icon_copy,\n icon_download,\n icon_retract,\n icon_exp"
},
{
"path": "package/zustand_generator/src/component/ChromogenZustandObserver.tsx",
"chars": 1174,
"preview": "import Editor from './Editor';\nimport EditorTab from './EditorTab';\nimport React, { useState } from 'react';\nimport { ge"
},
{
"path": "package/zustand_generator/src/component/Editor.tsx",
"chars": 1798,
"preview": "import React, { useState } from 'react';\nimport CodeEditor from '@uiw/react-textarea-code-editor';\nimport NumberList fro"
},
{
"path": "package/zustand_generator/src/component/EditorTab.tsx",
"chars": 472,
"preview": "import React from 'react';\nimport SecondaryButton from './Buttons/SecondaryButton';\n\ntype Props = {\n setIsHidden: Funct"
},
{
"path": "package/zustand_generator/src/component/Header.tsx",
"chars": 4630,
"preview": "import React, { useEffect, useState } from 'react';\nimport SecondaryButton from './Buttons/SecondaryButton';\nimport { ge"
},
{
"path": "package/zustand_generator/src/component/Icons.tsx",
"chars": 3735,
"preview": "import React from 'react';\n\nexport const icon_download = (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\""
},
{
"path": "package/zustand_generator/src/component/Numbers.tsx",
"chars": 976,
"preview": "import React from 'react';\n\nconst listStyle: React.CSSProperties = {\n paddingBlock: '28px',\n display: 'flex',\n flexDi"
},
{
"path": "package/zustand_generator/src/component/Resizing/Resizer.tsx",
"chars": 1289,
"preview": "import React, { useEffect, useState } from 'react';\nimport styled from 'styled-components';\n\nconst ResizerStyle = styled"
},
{
"path": "package/zustand_generator/src/component/component-utils.ts",
"chars": 2202,
"preview": "/* eslint-disable */\nimport type { CSSProperties } from 'react';\n// import type { SerializableParam } from 'recoil';\nimp"
},
{
"path": "package/zustand_generator/src/component/panel.tsx",
"chars": 4254,
"preview": "/* eslint-disable */\nimport React, { useState, useEffect } from 'react';\nimport { useStore } from '../utils/store';\nimpo"
},
{
"path": "package/zustand_generator/src/output/output-utils.ts",
"chars": 3798,
"preview": "/* eslint-disable */\nimport type { Transaction, InitialRender } from '../types';\n// } from '../types';\n\nimport { dummyPa"
},
{
"path": "package/zustand_generator/src/output/output.ts",
"chars": 1647,
"preview": "/* eslint-disable */\nimport type { Ledger } from '../types';\nimport { importZustandStore, testInitialState, testStateCha"
},
{
"path": "package/zustand_generator/src/types.ts",
"chars": 474,
"preview": "// ----- EXPORTING TYPES TO BE USED IN SRC/.TSX FILES -----\n\ntype NotAFunction = { [k: string]: unknown } & ({ bind?: ne"
},
{
"path": "package/zustand_generator/src/utils/ledger.ts",
"chars": 117,
"preview": "import type { Ledger } from '../types';\n\nexport const ledger: Ledger = {\n initialRender: {},\n transactions: [],\n};\n"
},
{
"path": "package/zustand_generator/src/utils/store.ts",
"chars": 387,
"preview": "import create from 'zustand';\n\ninterface RecordingState {\n recording: boolean;\n toggleRecording: () => void;\n}\n\n/*\nAll"
},
{
"path": "package/zustand_generator/src/utils/utils.ts",
"chars": 49,
"preview": "export const dummyParam = 'chromogenDummyParam';\n"
},
{
"path": "package.json",
"chars": 3388,
"preview": "{\n \"name\": \"chromogen-root\",\n \"version\": \"5.0.1\",\n \"description\": \"simple, interaction-driven test generator for Reco"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the oslabs-beta/Chromogen GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 103 files (229.2 KB), approximately 64.1k tokens, and a symbol index with 68 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.