{"id":14969949,"url":"https://github.com/open-source-labs/chromogen","last_synced_at":"2025-04-04T19:14:16.652Z","repository":{"id":41417702,"uuid":"285941357","full_name":"open-source-labs/Chromogen","owner":"open-source-labs","description":"UI-driven Jest test-generation package for Recoil selectors and Zustand store hooks","archived":false,"fork":false,"pushed_at":"2023-03-02T22:33:14.000Z","size":30778,"stargazers_count":280,"open_issues_count":6,"forks_count":52,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-04T19:14:07.384Z","etag":null,"topics":["jest","react","recoil","testing-tools","zustand"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/open-source-labs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-08-07T23:57:50.000Z","updated_at":"2024-08-21T04:03:24.000Z","dependencies_parsed_at":"2024-01-15T21:01:57.807Z","dependency_job_id":null,"html_url":"https://github.com/open-source-labs/Chromogen","commit_stats":{"total_commits":551,"total_committers":38,"mean_commits":14.5,"dds":0.8602540834845736,"last_synced_commit":"6b2085e43c65c20a90916b22255b2f1606411682"},"previous_names":["oslabs-beta/chromogen"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/open-source-labs%2FChromogen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/open-source-labs%2FChromogen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/open-source-labs%2FChromogen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/open-source-labs%2FChromogen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/open-source-labs","download_url":"https://codeload.github.com/open-source-labs/Chromogen/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247234923,"owners_count":20905854,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jest","react","recoil","testing-tools","zustand"],"created_at":"2024-09-24T13:42:44.684Z","updated_at":"2025-04-04T19:14:16.630Z","avatar_url":"https://github.com/open-source-labs.png","language":"TypeScript","readme":"\u003cdiv align=\"center\"\u003e\n\n\u003ca href=\"https://chromogen-site-eight.vercel.app/\"\u003e\n  \u003cimg\n    height=\"200\"\n    width=\"450\"\n    alt=\"Chromogen logo\"\n    src=\"./assets/logo/Chromogen.png\"\n  /\u003e\n\u003c/a\u003e\n\n\u003ch3\u003eA UI-driven test-generation package for \u003ca href= https://github.com/pmndrs/zustand\u003e Zustand\u003c/a\u003e Stores and \u003ca href=\"https://github.com/facebookexperimental/Recoil\"\u003eRecoil.js\u003c/a\u003e selectors.\u003c/h3\u003e\n\n\u003cbr /\u003e\n\n[![npm version](https://img.shields.io/npm/v/chromogen)](https://www.npmjs.com/package/chromogen)\n[![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/open-source-labs/Chromogen/blob/master/LICENSE)\n[![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=CHROMOGEN%20-%20A%20UI-driven%20Jest%20test%20generator%20for%20Recoil%20apps%0A\u0026url=https://www.npmjs.com/package/Chromogen\u0026hashtags=React,Recoil,Jest,testing)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)\n[![npm downloads](https://img.shields.io/npm/dm/chromogen)](https://www.npmjs.com/package/chromogen)\n[![Github stars](https://img.shields.io/github/stars/open-source-labs/Chromogen?style=social)](https://github.com/open-source-labs/Chromogen)\n\u003cbr /\u003e\n\n\u003c/div\u003e\n\u003cbr /\u003e\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Supported Tests](#supported-tests)\n- [Installing the Package](#installing-the-package)\n- [Installation for Zustand Apps](#installation-for-zustand-apps)\n- [Installation for Recoil Apps](#installation-for-recoil-apps)\n- [Usage for All Apps](#usage-for-all-apps)\n- [Contributor Setup](#contributor-setup)\n- [Test Setup](#test-setup)\n- [CI/CD with Jenkins](#jenkins)\n- [Demo Apps](#demo-apps)\n- [Contributing](#contributing)\n- [Core Team](#core-team)\n- [License](#license)\n  \u003cBr\u003e\u003cbr /\u003e\n\n## Overview\n\nYou'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.\n\u003cbr\u003e\u003cBr\u003e\n\n[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.\n\u003cb\u003e Chromogen is now compatible with React V18! \u003c/b\u003e\n\n\u003cbr\u003e\u003chr\u003e\n\n## Supported Tests\n\n\u003cb\u003eZustand Tests\u003c/b\u003e\n\nChromogen currently supports two types of testing for Zustand applications:\n\n1. **Initial Store State** on page load.\n2. **Store State Changes** whenever an action is invoked on the store.\n\nOn 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.\n\nTo use Chromogen with your Zustand application, please see the [Installation for Zustand Apps](#installation-for-zustand-apps) section below.\n\n\u003cbr\u003e\n\u003cb\u003eRecoil Tests\u003c/b\u003e\n\nChromogen currently supports three main types of tests for Recoil apps:\n\n1. **Initial selector values** on page load\n2. **Selector return values** for a given state, using snapshots captured after each state transaction.\n3. **Selector _set_ logic** asserting on resulting atom values for a given `newValue` argument and starting state.\n\nThese 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.\n\nAt 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.\n\nBy default, Chromogen uses atom and selector keys to populate the import \u0026 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\ncomponent as a `store` prop. The installation instructions below contain further details.\n\n\u003cbr\u003e\u003chr\u003e\n\n## Installing the Package\n\n```\nnpm install chromogen\n```\n\n\u003cbr\u003e\u003chr\u003e\n\n## Installation for Zustand Apps\n\nBefore using Chromogen, you'll need to make two changes to your application:\n\n1. Import the `\u003cChromogenZustandObserver /\u003e` component and render it alongside any other components in `\u003cApp /\u003e`\n2. Import `chromogenZustandMiddleware` function from Chromogen. This will be used as middleware when setting up your store.\n\n### Import the ChromogenZustandObserver component\n\nImport `ChromogenZustandObserver`. ChromogenZustandObserver can be rendered alongside any other components in `\u003cApp /\u003e`.\n\n```jsx\nimport React from 'react';\nimport { ChromogenZustandObserver } from 'chromogen';\nimport TodoList from './TodoList';\n\nconst App = () =\u003e (\n  \u003c\u003e\n    \u003cChromogenZustandObserver\u003e\n    \u003cTodoList /\u003e\n    \u003c/ChromogenZustandObserver\u003e\n  \u003c/\u003e\n);\n\nexport default App;\n```\n\nImport `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.\n\n1. _Overwrite State_ (boolean) - Without middleware, this defaults to `false`, but you'll need to explicitly provide a value when using Chromogen.\n2. _Action Name_ - Used for test generation\n3. _Action Parameters_ - If the action requires input parameters, pass these in after the Action Name.\n\n```jsx\nimport { chromogenZustandMiddleware } from 'chromogen';\nimport create from 'zustand';\n\nconst useStore = create(\n  chromogenZustandMiddleware((set) =\u003e ({\n    counter: 0,\n    color: 'black',\n    prioritizeTask: ['walking', 5],\n    addCounter: () =\u003e set(() =\u003e ({ counter: (counter += 1) }), false, 'addCounter'),\n    changeColor: (newColor) =\u003e set(() =\u003e ({ color: newColor }), false, 'changeColor', newColor),\n    setTaskPriority: (task, priority) =\u003e\n      set(() =\u003e ({ prioritizeTask: [task, priority] }), false, 'setTaskPriority', task, priority),\n  })),\n);\n\nexport default useStore;\n```\n\n\u003cbr\u003e\u003chr\u003e\n\n## Installation for Recoil Apps\n\nBefore running Chromogen, you'll need to make two changes to your application:\n\n1. Import the `\u003cChromogenObserver /\u003e` component as a child of `\u003cRecoilRoot /\u003e`\n1. Import the `atom` and `selector` functions from Chromogen instead of Recoil\n\n\u003ci\u003eNote: These changes do have a small performance cost, so they should be reverted before deploying to production.\u003c/i\u003e\n\n\u003cbr\u003e\n\n### Import the ChromogenObserver component\n\nChromogenObserver 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.\n\n```jsx\nimport React from 'react';\nimport { RecoilRoot } from 'recoil';\nimport { ChromogenObserver } from 'chromogen';\nimport MyComponent from './components/MyComponent.jsx';\n\nconst App = (props) =\u003e (\n  \u003cRecoilRoot\u003e\n    \u003cChromogenObserver /\u003e\n    \u003cMyComponent {...props} /\u003e\n  \u003c/RecoilRoot\u003e\n);\n\nexport default App;\n```\n\nIf 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:\n\n```jsx\nimport * as store from './store';\n// ...\n\u003cChromogenObserver store={store} /\u003e;\n```\n\nIf your store utilizes seprate files for various pieces of state, you can pass all of the imports in an array:\n\n```jsx\nimport * as atoms from './store/atoms';\nimport * as selectors from './store/selectors';\nimport * as misc from './store/arbitraryRecoilState';\n// ...\n\u003cChromogenObserver store={[atoms, selectors, misc]} /\u003e;\n```\n\n\u003cbr\u003e\n\n### Import atom \u0026 selector functions from Chromogen\n\nWherever 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.\n\n```js\nimport { atom, selector } from 'chromogen';\n\nexport const fooState = atom({\n  key: 'fooState',\n  default: {},\n});\n\nexport const barState = selector({\n  key: 'barState',\n  get: ({ get }) =\u003e {\n    const derivedState = get(fooState);\n    return derivedState.baz || 'value does not exist';\n  },\n});\n```\n\n\u003cbr\u003e\u003chr\u003e\n\n## Usage for All Apps\n\nAfter following the installation steps above, launch your application as normal. You should see two buttons in the bottom left corner.\n\n\u003cdiv align=\"center\"\u003e\n\n![Buttons](./assets/README-root/ultratrimmedDemo.gif)\n\n\u003c/div\u003e\n\nThe 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.\n\nThe 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.\n\nThe 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.\n\nOnce 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**\n\nYou're now ready to run your tests! After running your normal Jest test command, you should see a test suite for `chromogen.test.js`.\n\nThe current tests check whether state has changed after an interaction and checks whether the resulting state change variables have been updated as expected.\n\n\u003cbr\u003e\u003chr\u003e\n## Contributor Setup\n\t\nIn order to make/observe changes to the code, you'll have to run Chromogen locally as opposed to running via NPM.\nDue to inconsistencies across different machines, it is recomended to use the following method to run Chromogen locally.\n\n\u003cbr\u003e\u003cBr\u003e\n**Run for demos within this directory**\n\nAfter cloning the repo, \n\t\n```jsx\n\tnpm install\n```\n\t\nfrom BOTH the /package directory (where the app lives) AND the /demo directory you're developing with.\n\t\nThen, and ONLY then, run \n\t\n```jsx\nnpm run tarballUpdate\t\n```\n\u003cbr\u003e\u003cBr\u003e\n\t\n**Run for local applications outside this directory**\n\nAfter cloning this repo, add the following script to your app's package.json:\n\t\n```jsx\n\"tarballUpdate\": \"npm --prefix \u003creference to the /package directory on your local machine\u003e run build \u0026\u0026 npm pack \u003creference to the /package directory on your local machine\u003e \u0026\u0026 npm uninstall chromogen \u0026\u0026 npm install ./chromogen-5.0.1.tgz \u0026\u0026 npm start\"\n```\n\t\n\u003cbr\u003e\u003chr\u003e\t\n## Test Setup\n\n### Zustand Test Setup\n\nBefore running the test file, you'll need to specify the import path for your store by replacing `\u003cADD STORE FILEPATH\u003e`. 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.\n\n|                              **BEFORE**                               |                               **AFTER**                               |\n| :-------------------------------------------------------------------: | :-------------------------------------------------------------------: |\n| ![Default Filepath](./assets/README-root/zustand-test-filepath-1.png) | ![Updated Filepath](./assets/README-root/zustand-test-filepath-2.png) |\n\n\u003cdiv align=\"center\"\u003e\n\n![Test Output](./assets/README-root/zustand-test-snapshot-2.png)\n\n\u003c/div\u003e\n\n\u003cbr\u003e\n\n---\n\n### Recoil Test Setup\n\nBefore running the test file, you'll need to specify the import path for your store by replacing `\u003cADD STORE FILEPATH\u003e`. 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.\n\n|                          **BEFORE**                           |                          **AFTER**                           |\n| :-----------------------------------------------------------: | :----------------------------------------------------------: |\n| ![Default Filepath](./assets/README-root/filepath-before.png) | ![Updated Filepath](./assets/README-root/filepath-after.png) |\n\nYou're now ready to run your tests! Upon running your normal Jest test command, you should see three suites for `chromogen.test.js`:\n\n\u003cdiv align=\"center\"\u003e\n\n![Test Output](./assets/README-root/test-output.png)\n\n\u003c/div\u003e\n\n**Initial Render** tests whether each selector returns the correct value at launch. There is one test per selector.\n\n**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.\n\n**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.\n\n\u003cbr\u003e\u003chr\u003e\n\t\n## CI/CD with Jenkins\n\t\nYou will need to have Docker installed to run Jenkins. Ru the following command to create a bridge network for Jenkins:\n\t\n```jsx\n\t\n\tdocker network create jenkins\n```\n\n\t\nEnable Docker commands to be executable with Jenkins nodes:\n\t\n```jsx\n\t\ndocker run \\\n  --name jenkins-docker \\\n  --rm \\\n  --detach \\\n  --privileged \\\n  --network jenkins \\\n  --network-alias docker \\\n  --env DOCKER_TLS_CERTDIR=/certs \\\n  --volume jenkins-docker-certs:/certs/client \\\n  --volume jenkins-data:/var/jenkins_home \\\n  --publish 2376:2376 \\\n  --publish 3003:3003 --publish 5003:5003 \\\n  docker:dind \\\n  --storage-driver overlay2\n\t\n\t\n```\n\t\nBuild a Docker image from the DOckerfile within Chromogen:\n\t\n```jsx\n\t\n\tdocker build -t chromogen-jenkins .\n\t\n```\n\t\nRun your Chromogen-Jenkins image in Docker as a container:\n\t\n```jsx\n\tdocker run \\\n  --name jenkins-blueocean \\\n  --detach \\\n  --network jenkins \\\n  --env DOCKER_HOST=tcp://docker:2376 \\\n  --env DOCKER_CERT_PATH=/certs/client \\\n  --env DOCKER_TLS_VERIFY=1 \\\n  --publish 8080:8080 \\\n  --publish 50000:50000 \\\n  --volume jenkins-data:/var/jenkins_home \\\n  --volume jenkins-docker-certs:/certs/client:ro \\\n  --volume \"$HOME\":/home \\\n  --restart=on-failure \\\n  --env JAVA_OPTS=\"-Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true\" \\\n  myjenkins-blueocean:2.375.3-1\t\n```\n\t\nNavigate to your localhost:8080 and enter the password (between the two sets of asterisks) that is generated from the following command:\n\t\n```jsx\n\tdocker logs jenkins-blueocean\t\n\t\n```\n\t\nCreate your first administrator user.\n\t\nStop and start your Docker container using one of the following:\n\t\n```jsx\n\t\n\tdocker stop jenkins-blueocean jenkins-docker\n\tdocker start jenkins-blueocean jenkins-docker\n\t\n```\n\nWhen 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'.\n\t\n\t\n\n\n\t\n## Demo Apps\n\n### Zustand Demo To-Do App\n\nChromogen'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 \u003ca href='https://demo-zustand-todo.vercel.app/'\u003ehere\u003c/a\u003e, and view the source code in the `demo-zustand-todo` folder of this repository.\n\n\u003cbr\u003e\n\n### Recoil Demo To-Do App\n\nChromogen'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 \u0026\u0026 npm start` to launch.\n\n\u003cbr\u003e\u003chr\u003e\n\n## Contributing\n\n**We expect all contributors to abide by the standards of behavior outlined in the [Code of Conduct](CODE_OF_CONDUCT.md).**\n\nWe 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.\n\n## Bugs\n\nPlease [file an issue](https://docs.github.com/en/github/managing-your-work-on-github/creating-an-issue) for bugs, missing documentation, or unexpected behavior.\n\n## Feature Requests\n\nPlease file an issue to suggest new features. Vote on feature requests by adding\na 👍. This helps us prioritize what to work on.\n\n## Questions\n\nFor any questions and concerns related to using the package, feel free to email us via `chromogen.app@gmail.com`.\n\u003cbr\u003e\u003cBr\u003e\n## Chromogen V5.0 updates\n\u003cbr\u003e\u003chr\u003e\n**GUI Overhaul**\n\n**Why?**\n  \n    *Hovering GUI blocked functionality of host app \n    *Recording/downloading interactivity was cumbersome and inflexible \n    *Suboptimal for CI/CD implementation Buttons not functional \n  \n**What?**\n  \n    *Discrete Collapsible IDE that allows for real-time observation \u0026 manual interactivity of generated tests \n  \n**Next steps:**\n  \n    *Recording button functionality\n  \u003cbr\u003e\u003cBr\u003e\u003cbr\u003e\u003chr\u003e\n  \n **Real-time feed rendering**\n\n**Why?**\n  \n    *Generated tests were only accessible as a monolith of text, preventing isolation of individual components’ tests\n  \n  **What?**\n  \n    *IDE updates in real-time as changes of state are recorded \n  \n  **Next steps:**\n  \n    *Test categorization. \n    *Filter groups of tests by: \n\n    *Initialization vs ΔState Action \n\n    *Description \n  \n    *and allow user to select which filter to apply to displayed generated tests.\n\u003cbr\u003e\u003cBr\u003e\u003cbr\u003e\u003chr\u003e\n   **CI/CD overhaul**\n\n**Why?**\n  \n    *Travis implementation not functional\n  \n  **What?**\n  \n    *Re-implemented CI/CD with Jenkins\n  \n  \u003cbr\u003e\u003cBr\u003e\u003cbr\u003e\u003chr\u003e\n\n  \n **Additional Next Steps**\n\n **Add functionality for Zustand multi-store rendering \u0026 Asynchronous state**\n\t\n**Docker containerization**\n  \n  **Why?**\n  \n    *V 4.0 presented inconsistencies when accessed from different local machines. \n\tThis hindered team workflow both with development and production-use\n  \n  **What?**\n  \n    *Containerization of app ensures homogenous, improved User/Dev experience\n\n \u003cbr\u003e \u003cbr\u003e \u003cbr\u003e \u003chr\u003e\n## Core Team\n\n\u003cbr\u003e\n\n\u003ctable\u003e\n  \u003ctr align=\"center\"\u003e\n   \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/sirbrachthepale\"\u003e\u003cimg src=\"https://ca.slack-edge.com/T047AGRDFG8-U04EFS600F2-6758e04a3dcc-512\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eBrach Burdick\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/dnvt\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/60344684?s=400\u0026u=7fa22ae1486df42eaf172c3f08941416603387c0\u0026v=4\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eFrancois Denavaut\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n     \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/maggiekwan\"\u003e\u003cimg src=\"https://ca.slack-edge.com/T047AGRDFG8-U046ZLFULCC-533eb79ef8c7-512\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eMaggie Kwan\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/Lawliang\"\u003e\u003cimg src=\"https://ca.slack-edge.com/T047AGRDFG8-U04CQEBF85B-267e9eba74d2-512\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eLawrence Liang\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/michellebholland\"\u003e\u003cimg src=\"https://avatars3.githubusercontent.com/u/64747593\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eMichelle Holland\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/andywang23\"\u003e\u003cimg src=\"https://avatars1.githubusercontent.com/u/64433815\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eAndy Wang\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/connorrose\"\u003e\u003cimg src=\"https://avatars1.githubusercontent.com/u/42079810\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eConnor Rose Delisle\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/chenchingk\"\u003e\u003cimg src=\"https://avatars0.githubusercontent.com/u/40308081\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eJim Chen\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n   \u003c!-- SPACE --\u003e\n  \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/amyy98\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/68040348?v=4\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eAmy Yee\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n     \u003c!-- SPACE --\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/wlstjs\"\u003e\u003cimg src=\"https://avatars1.githubusercontent.com/u/68680285?s=400\u0026u=5b89d376d4d27a77442b74dcfe1c9c4025ce6453\u0026v=4\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eJinseon Shin\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n   \u003c!-- SPACE --\u003e\n       \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/rtumel123\"\u003e\u003cimg src=\"https://i.postimg.cc/MGDTWMhQ/Ryan.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eRyan Tumel\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n       \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/cgreer011\"\u003e\u003cimg src=\"https://i.postimg.cc/qMPgQdsz/cam.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eCameron Greer\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n          \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/nicholasjs\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/59386257?v=4\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eNicholas Shay\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr align=\"center\"\u003e\n    \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/mp-04\"\u003e\u003cimg src=\"https://i.postimg.cc/nz6GjXXV/mp.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eMarcellies Pettiford\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/smk53664\"\u003e\u003cimg src=\"https://i.postimg.cc/mrRkfN64/sung.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eSung Kim\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/lina4lee\"\u003e\u003cimg src=\"https://i.postimg.cc/bJwvdYhF/lina.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eLina Lee\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/ericaysoh\"\u003e\u003cimg src=\"https://i.postimg.cc/76tZzvPP/erica.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eErica Oh\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/dtalmaraz\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/94757231?v=4\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eDani Almaraz\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n    \u003ctr align=\"center\"\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/crgb0s\"\u003e\u003cimg src=\"https://i.postimg.cc/BbpvdSJD/craig.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eCraig Boswell\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/Hali3030\"\u003e\u003cimg src=\"https://i.postimg.cc/xTj7Yf0C/hussein.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eHussein Ahmed\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/iannkila\"\u003e\u003cimg src=\"https://i.postimg.cc/rwcBD1C8/ian.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eIan Kila\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n    \u003c!-- SPACE --\u003e\n         \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/yuehaowong\"\u003e\u003cimg src=\"https://i.postimg.cc/T2JqRnwj/yuehao.jpg\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eYuehao Wong\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003c/td\u003e\n     \u003c/tr\u003e            \n\u003c/tr\u003e\n\u003c/table\u003e\n\u003cbr\u003e\u003cbr\u003e\n\n## LICENSE\n\nLogo crafted with [AdobeExpress](https://www.adobe.com/express/)\n\nREADME 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).\n\nAll Chromogen source code is [MIT](./LICENSE) licensed.\n\nLastly, shoutout to [this repo](https://github.com/conorhastings/redux-test-recorder) for the original inspiration.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopen-source-labs%2Fchromogen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopen-source-labs%2Fchromogen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopen-source-labs%2Fchromogen/lists"}