Repository: oslabs-beta/Recoilize Branch: staging Commit: 1af38cbd8131 Files: 94 Total size: 303.2 KB Directory structure: gitextract_l6qwon48/ ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── README_KO.md ├── babel.config.js ├── docs/ │ └── pull_request_template.md ├── mock/ │ ├── snapshot.js │ └── state-snapshot.js ├── package/ │ ├── .npmignore │ ├── README.md │ ├── formatFiberNodes.js │ ├── formatFiberNodes.ts │ ├── index.js │ ├── index.ts │ └── package.json ├── package.json ├── src/ │ ├── README.md │ ├── app/ │ │ ├── Containers/ │ │ │ ├── ButtonsContainer.tsx │ │ │ ├── MainContainer.tsx │ │ │ ├── SnapshotContainer.tsx │ │ │ ├── TravelContainer.tsx │ │ │ ├── VisualContainer.tsx │ │ │ └── __tests__/ │ │ │ ├── MainContainer.unit.test.js │ │ │ ├── SnapshotContainer.unit.test.js │ │ │ └── VisualContainer.unit.test.js │ │ ├── components/ │ │ │ ├── App.tsx │ │ │ ├── AtomNetwork/ │ │ │ │ ├── AtomNetwork.tsx │ │ │ │ ├── AtomNetworkLegend.tsx │ │ │ │ ├── AtomNetworkVisual.tsx │ │ │ │ └── __tests__/ │ │ │ │ ├── Network.unit.test.js │ │ │ │ └── __snapshots__/ │ │ │ │ └── Network.unit.test.js.snap │ │ │ ├── ComponentGraph/ │ │ │ │ ├── AtomComponentContainer.tsx │ │ │ │ ├── AtomComponentVisual.tsx │ │ │ │ └── __tests__/ │ │ │ │ ├── AtomComponentContainer.unit.test.js │ │ │ │ ├── AtomComponentVisual.unit.test.js │ │ │ │ └── AtomSelectorLegend.unit.test.js │ │ │ ├── Metrics/ │ │ │ │ ├── ComparisonGraph.tsx │ │ │ │ ├── FlameGraph.js │ │ │ │ ├── MetricsContainer.tsx │ │ │ │ ├── RankedGraph.tsx │ │ │ │ └── __tests__/ │ │ │ │ ├── IcicleVerticle.unit.test.js │ │ │ │ ├── Metrics.unit.test.js │ │ │ │ └── Visualizer.unit.test.js │ │ │ ├── NavBar/ │ │ │ │ ├── NavBar.tsx │ │ │ │ └── __test__/ │ │ │ │ └── Navbar.unit.test.js │ │ │ ├── Settings/ │ │ │ │ ├── AtomSettings.tsx │ │ │ │ ├── SettingsContainer.tsx │ │ │ │ ├── ThrottleSettings.tsx │ │ │ │ └── __tests__/ │ │ │ │ ├── AtomSettings.unit.test.js │ │ │ │ ├── StateSettings.unit.test.js │ │ │ │ ├── ThrottleSettings.unit.test.js │ │ │ │ └── __snapshots__/ │ │ │ │ ├── StateSettings.unit.test.js.snap │ │ │ │ └── ThrottleSettings.unit.test.js.snap │ │ │ ├── Slider/ │ │ │ │ └── MainSlider.tsx │ │ │ ├── SnapshotList/ │ │ │ │ └── __tests__/ │ │ │ │ └── SnapshotList.unit.test.js │ │ │ ├── StateDiff/ │ │ │ │ ├── Diff.tsx │ │ │ │ └── __tests__/ │ │ │ │ ├── StateDiff.unit.test.js │ │ │ │ └── __snapshots__/ │ │ │ │ └── StateDiff.unit.test.js.snap │ │ │ ├── StateTree/ │ │ │ │ ├── Tree.tsx │ │ │ │ └── __tests__/ │ │ │ │ └── Tree.unit.test.js │ │ │ └── Testing/ │ │ │ ├── CodeResults.js │ │ │ ├── Editor.js │ │ │ ├── SelectorsButton.tsx │ │ │ ├── TestingContainer.tsx │ │ │ ├── displayTests.tsx │ │ │ ├── dummySelector.js │ │ │ └── testing.css │ │ ├── index.tsx │ │ ├── state-management/ │ │ │ ├── __tests__/ │ │ │ │ └── slices.test.tsx │ │ │ ├── hooks.tsx │ │ │ ├── index.tsx │ │ │ └── slices/ │ │ │ ├── AtomNetworkSlice.tsx │ │ │ ├── AtomsAndSelectorsSlice.tsx │ │ │ ├── FilterSlice.tsx │ │ │ ├── SelectedSlice.tsx │ │ │ ├── SnapshotSlice.tsx │ │ │ ├── ThrottleSlice.tsx │ │ │ └── ZoomSlice.tsx │ │ └── utils/ │ │ ├── cleanComponentAtomTree.ts │ │ ├── makeRelationshipLinks.ts │ │ └── makeTreeConversion.ts │ ├── extension/ │ │ ├── background.ts │ │ ├── build/ │ │ │ ├── devtools.html │ │ │ ├── devtools.js │ │ │ ├── diff.css │ │ │ ├── manifest.json │ │ │ ├── panel.html │ │ │ └── stylesheet.css │ │ └── contentScript.ts │ └── types/ │ └── index.d.ts ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ module.exports = { env: { browser: true, es2020: true, }, extends: [ 'plugin:react/recommended', 'prettier', 'prettier/flowtype', // if you are using flow 'prettier/react', ], parser: 'babel-eslint', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 11, sourceType: 'module', }, plugins: ['flowtype', 'react', 'jsx-a11y', 'prettier'], rules: { 'prettier/prettier': ['error'], 'react/prop-types': 'off', }, settings: { react: { version: 'detect', }, }, }; ================================================ FILE: .gitignore ================================================ node_modules package-lock.json .vscode src/extension/build/bundles .DS_Store package/node_modules/ ================================================ FILE: .prettierrc ================================================ { "arrowParens": "avoid", "singleQuote": true, "trailingComma": "all", "bracketSpacing": false, "jsxBracketSameLine": false, "endOfLine": "auto", "overrides": [ { "files": ["*.js", "*.jsx"], "options": { "parser": "flow" } } ] } ================================================ 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 ================================================

Debugger for Recoil Applications

# [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/oslabs-beta/Recoilize/blob/staging/LICENSE) [![npm version](https://img.shields.io/npm/v/recoilize)](https://www.npmjs.com/package/recoilize) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) [Korean README 한국어](README_KO.md)

About

Recoilize is a Chrome Dev Tool meant for debugging applications built with the experimental Recoil.js state management library. The tool records Recoil state and allows users to easily debug their applications with features such as: time travel to previous states, visualization of the component graph and display of the atom selector network.

Download Recoilize from the Chrome Store

Visit the Recoilize landing page to demo

** STILL IN BETA **

Please note that Recoilize is in BETA. We will continue to make improvements and implement fixes but if you find any issues, please dont hesitate to report them in the issues tab or submit a PR and we'll happily take a look.

Installation

#### Install Recoilize Module ```js npm install recoilize ``` ### ** IMPORTANT ** #### Import RecoilizeDebugger from the Recoilize module ```js import RecoilizeDebugger from 'recoilize'; ``` #### Integrate RecoilizeDebugger as a React component within the recoil root: ```js import RecoilizeDebugger from 'recoilize'; import RecoilRoot from 'recoil'; ReactDOM.render( , document.getElementById('root'), ); ``` #### Please note, Recoilize assumes that the HTML element used to inject your React application has an ID of 'root'. If it does not the HTML element must be passed in as an attribute called 'root' to the RecoilizeDebugger component #### Example: ```js import RecoilizeDebugger from 'recoilize'; import RecoilRoot from 'recoil'; //If your app injects on an element with ID of 'app' const app = document.getElementById('app'); ReactDOM.render( , app, ); ``` ### In order to integrate Next.js applications with RecoilizeDebugger, follow the example below. ```js //If your application uses Next.js modify the _app.js as follows import dynamic from 'next/dynamic'; import { useEffect, useState } from 'react'; import { RecoilRoot } from 'recoil'; function MyApp({ Component, pageProps }) { const [root, setRoot] = useState(null) const RecoilizeDebugger = dynamic( () => { return import('recoilize'); }, { ssr: false} ); useEffect(() => { if (typeof window.document !== 'undefined') { setRoot(document.getElementById('__next')); } }, [root]); return ( <> ); } export default MyApp; ``` #### Open your application on the Chrome Browser and start debugging with Recoilize! ##### (Only supported with React applications using Recoil as state management)

New Features for Version 3.0.0

Support for Recoil 0.1.3

Recoilize now supports the most recent update to the Recoil library and is backwards compatible with older versions of Recoil.

Time Travel with ease

If you had used Recoilize before, you would have noticed an annoying bug that sometimes breaks the app and won’t allow you to be productive. With the new version of Recoilize, that issue is forever gone. Users can now use the tool with confidence and worry-free.

The main mission of Recoilize 3.0 is to make it more user-friendly, so you will enjoy our brand new time travel feature — the time travel slider! Why click and scroll through snapshots when you can do it with a slider and some buttons, right?

Customizable Component Graph

This is one of the coolest updates of Recoilize 3.0. We understand that different users have different ways of thinking and visualizing, and for that reason, the component tree now is fully customizable. You can expand or collapse the components, choose vertical or horizontal displays or adjust the spacing between elements.

Better User Experience with Atom Network

The atom network is one of the key features that differentiate Recoil.js from other alternative state management libraries. However, the atom network will grow bigger together with the app. At some points, it will be unmanageable and hard to keep track of all of the atoms. To make this easier and pain-free, the new Atom Network will allow you to freely move and arrange them anywhere you want.

Snapshot Comparison

We understand that developers always develop an app with an optimization philosophy in mind. Component rendering time can be difficult to measure for two reasons. First, your computer and your web browser are not the same as mine, so the run-time can be vastly different. Second, it’s really hard to keep track of a long series of snapshots. You definitely don’t want to waste all of your time calculating the rendering time by hand.

With the new Recoilize, users can now save a series of state snapshots and use it later to analyze/compare with the current series.

Features

Support for Concurrent Mode

If a Suspense component was used as a placeholder during component renderings, those suspense components will display with a red border in the expanded component graph. This indicates that a component was suspended during the render of the selected snapshot.

Performance Metrics

In 'Metrics' tab, two graphs display component render times. The flame graph displays the time a component took to render itself, and all of its child components. The bar graph displays the individual render times of each component.

Throttle

In the settings tab, users are able to set throttle (in milliseconds) for large scale applications or any applications that changes state rapidly. The default is set at 70ms.

State Persistence

Recoilize allows the users to persist their application's state through a refresh or reload. At this time, the user is able to view the previous states in the dev tool, but cannot time travel to the states before refresh.

Additional Features

We will continue updating Recoilize alongside Recoil's updates!

Contributors

Bren Yamaguchi @github @linkedin

Saejin Kang @github @linkedin

Jonathan Escamila @github @linkedin

Sean Smith @github @linkedin

Justin Choo @github @linkedin

Anthony Lin @github @linkedin

Spenser Schwartz @github @linkedin

Steven Nguyen @github @linkedin

Henry Taing @github @linkedin

Seungho Baek @github @linkedin

Aaron Yang @github @linkedin

Jesus Vargas @github @linkedin

Davide Molino @github @linkedin

Taven Shumaker @github @linkedin

Janis Hernandez @github @linkedin

Jaime Baik @github @linkedin

Anthony Magallanes @github @linkedin

Edward Shei @github @linkedin

Nathan Bargers @github @linkedin

Scott Campbell @github @linkedin

Steve Hong @github @linkedin

Jason Lee @github @linkedin

Razana Nisathar @github @linkedin

Harvey Nguyen @github @linkedin

Joey Ma @github @linkedin

Leonard Lew @github @linkedin

Victor Wang @github @linkedin

================================================ FILE: README_KO.md ================================================

Recoil 애플리케이션을 위한 디버깅 개발도구

# [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/oslabs-beta/Recoilize/blob/staging/LICENSE) [![npm version](https://img.shields.io/npm/v/recoilize)](https://www.npmjs.com/package/recoilize) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) [영어 README](README.md)

Recoilize에 대해서

Recoilize는 Recoil 상태관리 라이브러리를 사용하여 만들어진 애플리케이션을 디버깅 할수있는 Chrome Dev Tool입니다. Recoil 상태를 기록하여 유저들이 애플리케이션을 편하게 디버깅 할수 있도록 도와주는 기능들을 가지고 있습니다. 리액트 컴포넌트를 시각화 하여 그래프로 보여줌과 동시에 스냅샷을 이요하여 이전 상태로 시간이동을 가능하게 만들어줄수있는 도구입니다.

크롬 스토어 에서 다운로드 받으실수 있습니다.

데모를 위해서는 Recoilize 웹사이트를 방문하십시오.

** 현재는 베타 버젼입니다 **

Recoilize는 현재 베타버젼 입니다. 툴을 계속 개선하고 새로운 이슈들을 수정해 나갈것이고, 혹시 다른 버그들이나 이슈들이 나타난다면 언제든지 이슈 탭에 글을 작성하시거나 PR을 해주시면 감사하겠습니다.

설치 방법

#### Recoilize 모듈 설치 ```js npm install recoilize ``` ### ** 중요 ** #### Recoilize모듈에서 RecoilizeDebugger를 import해줘야 합니다 ```js import RecoilizeDebugger from 'recoilize'; ``` #### RecoilizeDebugger를 recoil root 안에 리액트 컴포넌트로 넣어야 합니다 ```js import RecoilizeDebugger from 'recoilize'; import RecoilRoot from 'recoil'; ReactDOM.render( , document.getElementById('root'), ); ``` #### Recoilize는 리액트 애플리케이션을 주입시키기 위해 쓴 HTML 엘리먼트의 아이디를 'root'으로 가정합니다.아닐경우 RecoilizeDebugger에 'root'속성을 만들고 HTML 엘리먼트를 패스하십시오. ```js import RecoilizeDebugger from 'recoilize'; import RecoilRoot from 'recoil'; //If your app injects on an element with ID of 'app' const app = document.getElementById('app'); ReactDOM.render( , app, ); ``` #### 애플리케이션을 크롬 브라우저에서 열고 Recoilize 디버깅툴을 실행하시면 됩니다. ##### (현재 Recoil을 상태관리 라이브러리로 사용하는 리액트 애플리케이션만 지원합니다.)

새로운 기능

Recoil 0.1.2를 지원합니다

Recoilize는 최신 버전과 구버전의 Recoil과 호환이 됩니다

스냅샷 클리어

Previous와 Forward 버튼을 넣어 선택된 스냅샵의 전이나 후에 있는 스냅샷을 지울 수 있게 했습니다

컴포넌트 그래프

호버

그래프의 노드를 호버했을 때 안의 텍스트가 보이는 형태를 개선하였습니다

atom 범례

범례의 텍스트가 클릭되면 드롭다운 형태의 atom이나 selector 리스트가 보이게 하였습니다

드롭다운 리스트에 있는 각각의 atom이나 selector를 누를 경우 해당 atom이나 selector를 쓰는 컴포넌트가 하이라이트되도록 바꾸었습니다

atom 네트워크

atom 범례

범례의 텍스트가 클릭되면 드롭다운 형태의 atom이나 selector 리스트가 보이게 하였습니다

드롭다운 리스트에 있는 각각의 atom이나 selector를 누를 경우 관련 atom이나 selector 노드가 보이도록 했습니다

그래프

여러개의 그래프가 겹치지 않도록 조정하였습니다

검색 창

검색 창이 탐색 버튼과 겹치지 않도록 변경하였습니다

Ranked 그래프

애니메이션을 없애서 전과 후 상태비교가 쉽게 보이도록 바꾸었습니다

기능

Concurrent 모드 지원

만약 컴포넌트를 보류시키기 위해 Suspense 컴포넌트가 사용됐을 경우, 해당 컴포넌트의 노드 주위에 빨간 테두리로 표시하여 컴포넌트가 나타나기까지 지연되었음을 알려줄 것입니다

퍼포먼스 측정 그래프

'Metrics' 탭에 있는 두가지 그래프는 렌더링 시간을 보여줍니다

Flame 그래프는 각각의 컴포넌트와 자식 컴포넌트가 나타나기까지 걸린 합산된 시간을 보여주고 Ranked 그래프는 각각의 컴포넌트가 나오기까지 걸린 시간을 보여줍니다

시간 이동

Recoilize의 주요 기능 중 하나로, 이 도구는 사용자가 이전의 모든 스냅샷으로 이동할 수 있게 해줍니다. 각 스냅샷 옆에 있는 점프 버튼을 누르면 해당 스냅샷으로 상태를 설정하여 DOM이 변경됩니다.

시각화

사용자는 개별 스냅샷을 클릭하여 애플리케이션 상태에 대한 시각화된 그래프를 볼수있고, 컴포넌트 트리와 다른 그래프 뿐만 아니라 State tree를 JSON 형식으로 지원합니다

쓰로틀링

대규모 애플리케이션 또는 상태를 빠르게 변경하는 모든 애플리케이션에 대해 쓰로틀링(ms)을 설정할 수 있습니다. 기본값은 70ms로 설정되어 있습니다.

상태 유지 (베타)

Recoilize는 사용자가 새로 고침을 했을 경우에도 응용 프로그램의 상태를 유지할 수 있도록 해줍니다. 이때 사용자는 개발 도구에서는 이전 상태를 볼 수 있지만, 새로고침 전에 상태로의 시간 이동은 할 수 없습니다. 우리 팀은 여전히 이 기능을 완성하기 위해 노력하고 있습니다.

부가 기능

우리는 Recoil의 업데이트와 함께 Recoilize를 계속 업데이트 할 것 입니다

기여

Bren Yamaguchi @github @linkedin

Saejin Kang @github @linkedin

Jonathan Escamila @github @linkedin

Sean Smith @github @linkedin

Justin Choo @github @linkedin

Anthony Lin @github @linkedin

Spenser Schwartz @github @linkedin

Steven Nguyen @github @linkedin

Henry Taing @github @linkedin

Seungho Baek @github @linkedin

Aaron Yang @github @linkedin

Jesus Vargas @github @linkedin

Davide Molino @github @linkedin

Taven Shumaker @github @linkedin

Janis Hernandez @github @linkedin

Jaime Baik @github @linkedin

Anthony Magallanes @github @linkedin

Edward Shei @github @linkedin

================================================ FILE: babel.config.js ================================================ module.exports = { presets: [ '@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript', ], plugins: ['@babel/plugin-proposal-class-properties'], }; ================================================ FILE: docs/pull_request_template.md ================================================ ## Types of changes - [ ] Bugfix (change which fixes an issue) - [ ] New feature (change which adds functionality) - [ ] Refactor (change which changes the codebase without affecting its external behavior) - [ ] Non-breaking change (fix or feature that would causes existing functionality to work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to __not__ work as expected) ## Purpose ## Approach ## Resources ## Screenshot(s) ================================================ FILE: mock/snapshot.js ================================================ export const filteredCurSnapMock = { dummyAtom1: { contents: {hello: [], hi: []}, nodeDeps: [], nodeToNodeSubscriptions: [], type: 'RecoilState', }, listState: { contents: [{text: 'list item'}, {text: 'list item'}, {text: 'list item'}], nodeDeps: [], nodeToNodeSubscriptions: ['selectorTest', 'stateLengths'], type: 'RecoilState', }, listState2: { contents: [{text: 'list item'}, {text: 'list item'}, {text: 'list item'}], nodeDeps: [], nodeToNodeSubscriptions: ['stateLengths'], type: 'RecoilState', }, selectorTest: { contents: 'test', nodeDeps: ['listState'], nodeToNodeSubscriptions: [], type: 'RecoilValueReadOnly', }, stateLengths: { contents: 6, nodeDeps: ['listState', 'listState2'], nodeToNodeSubscriptions: [], type: 'RecoilValueReadOnly', }, }; export const filteredPrevSnapMock = { dummyAtom1: { contents: {hello: [], hi: []}, nodeDeps: [], nodeToNodeSubscriptions: [], type: 'RecoilState', }, listState: { contents: [{text: 'list item'}, {text: 'list item'}, {text: 'list item'}], nodeDeps: [], nodeToNodeSubscriptions: ['selectorTest', 'stateLengths'], type: 'RecoilState', }, listState2: { contents: [ {text: 'list item'}, {text: 'list item'}, {text: 'list item'}, {text: 'list item'}, ], nodeDeps: [], nodeToNodeSubscriptions: ['stateLengths'], type: 'RecoilState', }, selectorTest: { contents: 'test', nodeDeps: ['listState'], nodeToNodeSubscriptions: [], type: 'RecoilValueReadOnly', }, stateLengths: { contents: 6, nodeDeps: ['listState', 'listState2'], nodeToNodeSubscriptions: [], type: 'RecoilValueReadOnly', }, }; export const componentAtomTreeMock = { children: [ { children: [], name: '', tag: 0, }, { children: [ { children: [], name: '', tag: 0, }, { children: [ { children: [], name: '', tag: 0, }, { children: [ { children: [], name: '', tag: 0, }, ], name: '', tag: 0, }, ], name: '', tag: 0, }, ], name: '', tag: 0, }, { children: [ { children: [], name: '', tag: 0, }, { children: [], name: '', tag: 0, }, ], name: '', tag: 0, }, ], name: '', tag: 0, }; ================================================ FILE: mock/state-snapshot.js ================================================ export const snapshotHistoryMock = { snapshotHistory: [ { componentAtomTree: { actualDuration: 1.6750000004321919, children: [ { actualDuration: 1.6650000006848131, children: [ { actualDuration: 1.6650000006848131, children: [ { actualDuration: 1.6650000006848131, children: [ { actualDuration: 0, children: [], name: 'Batcher', recoilNodes: [], tag: 0, treeBaseDuration: 0.15499999972234946, wasSuspended: false, }, { actualDuration: 1.6650000006848131, children: [ { actualDuration: 0, children: [], name: 'RecoilizeDebugger', recoilNodes: [], tag: 0, treeBaseDuration: 1.4600000004065805, wasSuspended: false, }, { actualDuration: 1.6650000006848131, children: [ { actualDuration: 0.8450000004813774, children: [ { actualDuration: 0.6400000002031447, children: [ { actualDuration: 0.24500000017724233, children: [], name: 'p', recoilNodes: [], tag: 5, treeBaseDuration: 0.03500000002532033, wasSuspended: false, }, { actualDuration: 0.009999999747378752, children: [], name: 'p', recoilNodes: [], tag: 5, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, { actualDuration: 0.12000000060652383, children: [ { actualDuration: 0.054999999520077836, children: [], name: 'button', recoilNodes: [], tag: 5, treeBaseDuration: 0.030000000151630957, wasSuspended: false, }, { actualDuration: 0.03500000002532033, children: [], name: 'button', recoilNodes: [], tag: 5, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, ], name: 'div', recoilNodes: [], tag: 5, treeBaseDuration: 0.05500000042957254, wasSuspended: false, }, ], name: 'div', recoilNodes: [], tag: 5, treeBaseDuration: 0.3500000002532033, wasSuspended: false, }, ], name: 'PlaygroundStart', recoilNodes: [], tag: 0, treeBaseDuration: 0.5550000005314359, wasSuspended: false, }, ], name: 'PlaygroundRender', recoilNodes: ['playStart'], tag: 0, treeBaseDuration: 1.3750000007348717, wasSuspended: false, }, ], name: 'Fragment', recoilNodes: [], tag: 7, treeBaseDuration: 2.8550000006362097, wasSuspended: false, }, ], name: 'react.provider', recoilNodes: [], tag: 10, treeBaseDuration: 3.050000000257569, wasSuspended: false, }, ], name: 'react.provider', recoilNodes: [], tag: 10, treeBaseDuration: 3.0849999993733945, wasSuspended: false, }, ], name: 'RecoilRoot', recoilNodes: [], tag: 0, treeBaseDuration: 3.2199999996009865, wasSuspended: false, }, ], name: 'HR', recoilNodes: [], tag: 3, treeBaseDuration: 3.3249999996769475, wasSuspended: false, }, filteredSnapshot: { currentPlayerState: { contents: 'X', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, gameEndSelector: { contents: false, nodeDeps: [ 'square-0', 'square-1', 'square-2', 'square-3', 'square-4', 'square-5', 'square-6', 'square-7', 'square-8', 'currentPlayerState', ], nodeToNodeSubscriptions: [], type: 'RecoilValueReadOnly', }, playStart: { contents: false, nodeDeps: [], nodeToNodeSubscriptions: [], type: 'RecoilState', }, 'square-0': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-1': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-2': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-3': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-4': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-5': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-6': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-7': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-8': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, }, indexDiff: 0, }, { componentAtomTree: { actualDuration: 1.6750000004321919, children: [ { actualDuration: 1.6650000006848131, children: [ { actualDuration: 1.6650000006848131, children: [ { actualDuration: 1.6650000006848131, children: [ { actualDuration: 0, children: [], name: 'Batcher', recoilNodes: [], tag: 0, treeBaseDuration: 0.15499999972234946, wasSuspended: false, }, { actualDuration: 1.6650000006848131, children: [ { actualDuration: 0, children: [], name: 'RecoilizeDebugger', recoilNodes: [], tag: 0, treeBaseDuration: 1.4600000004065805, wasSuspended: false, }, { actualDuration: 1.6650000006848131, children: [ { actualDuration: 0.8450000004813774, children: [ { actualDuration: 0.6400000002031447, children: [ { actualDuration: 0.24500000017724233, children: [], name: 'p', recoilNodes: [], tag: 5, treeBaseDuration: 0.03500000002532033, wasSuspended: false, }, { actualDuration: 0.009999999747378752, children: [], name: 'p', recoilNodes: [], tag: 5, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, { actualDuration: 0.12000000060652383, children: [ { actualDuration: 0.054999999520077836, children: [], name: 'button', recoilNodes: [], tag: 5, treeBaseDuration: 0.030000000151630957, wasSuspended: false, }, { actualDuration: 0.03500000002532033, children: [], name: 'button', recoilNodes: [], tag: 5, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, ], name: 'div', recoilNodes: [], tag: 5, treeBaseDuration: 0.05500000042957254, wasSuspended: false, }, ], name: 'div', recoilNodes: [], tag: 5, treeBaseDuration: 0.3500000002532033, wasSuspended: false, }, ], name: 'PlaygroundStart', recoilNodes: [], tag: 0, treeBaseDuration: 0.5550000005314359, wasSuspended: false, }, ], name: 'PlaygroundRender', recoilNodes: ['playStart'], tag: 0, treeBaseDuration: 1.3750000007348717, wasSuspended: false, }, ], name: 'Fragment', recoilNodes: [], tag: 7, treeBaseDuration: 2.8550000006362097, wasSuspended: false, }, ], name: 'react.provider', recoilNodes: [], tag: 10, treeBaseDuration: 3.050000000257569, wasSuspended: false, }, ], name: 'react.provider', recoilNodes: [], tag: 10, treeBaseDuration: 3.0849999993733945, wasSuspended: false, }, ], name: 'RecoilRoot', recoilNodes: [], tag: 0, treeBaseDuration: 3.2199999996009865, wasSuspended: false, }, ], name: 'HR', recoilNodes: [], tag: 3, treeBaseDuration: 3.3249999996769475, wasSuspended: false, }, filteredSnapshot: { currentPlayerState: { contents: 'X', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, gameEndSelector: { contents: false, nodeDeps: [ 'square-0', 'square-1', 'square-2', 'square-3', 'square-4', 'square-5', 'square-6', 'square-7', 'square-8', 'currentPlayerState', ], nodeToNodeSubscriptions: [], type: 'RecoilValueReadOnly', }, playStart: { contents: false, nodeDeps: [], nodeToNodeSubscriptions: [], type: 'RecoilState', }, 'square-0': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-1': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-2': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-3': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-4': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-5': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-6': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-7': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, 'square-8': { contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], type: 'RecoilState', }, }, indexDiff: 0, }, { componentAtomTree: { name: 'HR', tag: 3, children: [ { name: 'RecoilRoot', tag: 0, children: [ { name: 'react.provider', tag: 10, children: [ { name: 'react.provider', tag: 10, children: [ { name: 'Batcher', tag: 0, children: [], recoilNodes: [], actualDuration: 0.004999999873689376, treeBaseDuration: 0.03500000002532033, wasSuspended: false, }, { name: 'Fragment', tag: 7, children: [ { name: 'RecoilizeDebugger', tag: 0, children: [], recoilNodes: [], actualDuration: 0.5049999999755528, treeBaseDuration: 0.5049999999755528, wasSuspended: false, }, { name: 'PlaygroundRender', tag: 0, children: [ { name: 'App', tag: 1, children: [ { name: 'div', tag: 5, children: [ { name: 'h1', tag: 5, children: [], recoilNodes: [], actualDuration: 0.05999999939376721, treeBaseDuration: 0, wasSuspended: false, }, { name: 'Board', tag: 0, children: [ { name: 'div', tag: 5, children: [ { name: 'Fragment', tag: 7, children: [ { name: 'Row', tag: 0, children: [ { name: 'div', tag: 5, children: [ { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.06000000030326191, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, ], recoilNodes: [ 'square-0', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.17500000012660166, treeBaseDuration: 0.11999999969702912, wasSuspended: false, }, { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.02499999936844688, treeBaseDuration: 0, wasSuspended: false, }, ], recoilNodes: [ 'square-1', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.07999999888852471, treeBaseDuration: 0.054999999520077836, wasSuspended: false, }, { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.02500000027794158, treeBaseDuration: 0.005000000783184078, wasSuspended: false, }, ], recoilNodes: [ 'square-2', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.06500000017695129, treeBaseDuration: 0.045000000682193786, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.3499999993437086, treeBaseDuration: 0.2299999996466795, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.40499999977328116, treeBaseDuration: 0.28500000007625204, wasSuspended: false, }, { name: 'Row', tag: 0, children: [ { name: 'div', tag: 5, children: [ { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.014999999621068127, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, ], recoilNodes: [ 'square-3', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.05999999939376721, treeBaseDuration: 0.04999999964638846, wasSuspended: false, }, { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.020000000404252205, treeBaseDuration: 0.005000000783184078, wasSuspended: false, }, ], recoilNodes: [ 'square-4', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.06000000030326191, treeBaseDuration: 0.045000000682193786, wasSuspended: false, }, { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.020000000404252205, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, ], recoilNodes: [ 'square-5', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.05500000042957254, treeBaseDuration: 0.03999999989900971, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.19500000053085387, treeBaseDuration: 0.1450000008844654, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.210000000151922, treeBaseDuration: 0.16000000050553354, wasSuspended: false, }, { name: 'Row', tag: 0, children: [ { name: 'div', tag: 5, children: [ { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.020000000404252205, treeBaseDuration: 0.005000000783184078, wasSuspended: false, }, ], recoilNodes: [ 'square-6', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.054999999520077836, treeBaseDuration: 0.03999999989900971, wasSuspended: false, }, { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.01500000053056283, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, ], recoilNodes: [ 'square-7', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.05500000042957254, treeBaseDuration: 0.044999999772699084, wasSuspended: false, }, { name: 'Box', tag: 0, children: [ { name: 'div', tag: 5, children: [], recoilNodes: [], actualDuration: 0.009999999747378752, treeBaseDuration: 0, wasSuspended: false, }, ], recoilNodes: [ 'square-8', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.03999999898951501, treeBaseDuration: 0.02499999936844688, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.15999999959603883, treeBaseDuration: 0.11499999982333975, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.17499999921710696, treeBaseDuration: 0.12999999944440788, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.7999999988896889, treeBaseDuration: 0.5849999997735722, wasSuspended: false, }, { name: 'h2', tag: 5, children: [], recoilNodes: [], actualDuration: 0.01500000053056283, treeBaseDuration: 0.005000000783184078, wasSuspended: false, }, { name: 'button', tag: 5, children: [], recoilNodes: [], actualDuration: 0.03999999989900971, treeBaseDuration: 0.004999999873689376, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.879999999597203, treeBaseDuration: 0.6050000010873191, wasSuspended: false, }, ], recoilNodes: ['gameEndSelector'], actualDuration: 1.059999999597494, treeBaseDuration: 0.7850000010876101, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 1.1449999992692028, treeBaseDuration: 0.8000000007086783, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 1.2199999991935329, treeBaseDuration: 0.8750000006330083, wasSuspended: false, }, ], recoilNodes: ['playStart'], actualDuration: 1.294999999117863, treeBaseDuration: 0.9500000005573384, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 1.7999999990934157, treeBaseDuration: 1.4750000000276486, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 1.804999998967105, treeBaseDuration: 1.5499999999519787, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 1.804999998967105, treeBaseDuration: 1.5849999990678043, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 1.804999998967105, treeBaseDuration: 1.7199999992953963, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 1.804999998967105, treeBaseDuration: 1.8249999993713573, wasSuspended: false, }, filteredSnapshot: { playStart: { type: 'RecoilState', contents: true, nodeDeps: [], nodeToNodeSubscriptions: [], }, 'square-0': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, 'square-1': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, 'square-2': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, 'square-3': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, 'square-4': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, 'square-5': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, 'square-6': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, 'square-7': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, 'square-8': { type: 'RecoilState', contents: '-', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, currentPlayerState: { type: 'RecoilState', contents: 'X', nodeDeps: [], nodeToNodeSubscriptions: ['gameEndSelector'], }, gameEndSelector: { type: 'RecoilValueReadOnly', contents: false, nodeDeps: [ 'square-0', 'square-1', 'square-2', 'square-3', 'square-4', 'square-5', 'square-6', 'square-7', 'square-8', 'currentPlayerState', ], nodeToNodeSubscriptions: [], }, }, indexDiff: 0, }, ], renderIndex: 2, cleanComponentAtomTree: { children: [ { name: 'Board', tag: 0, children: [ { name: 'Row', tag: 0, children: [ { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-0', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.17500000012660166, treeBaseDuration: 0.11999999969702912, wasSuspended: false, }, { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-1', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.07999999888852471, treeBaseDuration: 0.054999999520077836, wasSuspended: false, }, { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-2', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.06500000017695129, treeBaseDuration: 0.045000000682193786, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.40499999977328116, treeBaseDuration: 0.28500000007625204, wasSuspended: false, }, { name: 'Row', tag: 0, children: [ { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-3', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.05999999939376721, treeBaseDuration: 0.04999999964638846, wasSuspended: false, }, { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-4', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.06000000030326191, treeBaseDuration: 0.045000000682193786, wasSuspended: false, }, { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-5', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.05500000042957254, treeBaseDuration: 0.03999999989900971, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.210000000151922, treeBaseDuration: 0.16000000050553354, wasSuspended: false, }, { name: 'Row', tag: 0, children: [ { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-6', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.054999999520077836, treeBaseDuration: 0.03999999989900971, wasSuspended: false, }, { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-7', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.05500000042957254, treeBaseDuration: 0.044999999772699084, wasSuspended: false, }, { name: 'Box', tag: 0, children: [], recoilNodes: [ 'square-8', 'currentPlayerState', 'gameEndSelector', ], actualDuration: 0.03999999898951501, treeBaseDuration: 0.02499999936844688, wasSuspended: false, }, ], recoilNodes: [], actualDuration: 0.17499999921710696, treeBaseDuration: 0.12999999944440788, wasSuspended: false, }, ], recoilNodes: ['gameEndSelector'], actualDuration: 1.059999999597494, treeBaseDuration: 0.7850000010876101, wasSuspended: false, }, ], name: 'PlaygroundRender', recoilNodes: ['playStart'], tag: 0, actualDuration: 1.804999998967105, }, }; ================================================ FILE: package/.npmignore ================================================ __tests__ ./*.ts formatFiberNodes.ts index.ts node_modules/ ================================================ FILE: package/README.md ================================================

Debugger for Recoil Applications

# [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/oslabs-beta/Recoilize/blob/staging/LICENSE) [![npm version](https://img.shields.io/npm/v/recoilize)](https://www.npmjs.com/package/recoilize) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) # [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/oslabs-beta/Recoilize/blob/staging/LICENSE) [![npm version](https://img.shields.io/npm/v/recoilize)](https://www.npmjs.com/package/recoilize) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) [Korean README 한국어](README_KO.md)

About

Recoilize is a Chrome Dev Tool meant for debugging applications built with the experimental Recoil.js state management library. The tool records Recoil state and allows users to easily debug their applications with features such as time travel to previous states, visualization of the component graph and display of the atom selector network.

Download Recoilize from the Chrome Store

Visit the Recoilize landing page to demo

** STILL IN BETA **

Please note that Recoilize is in BETA. We will continue to make improvements and implement fixes but if you find any issues, please dont hesitate to report them in the issues tab or submit a PR and we'll happily take a look.

Installation

#### Install Recoilize Module ```js npm install recoilize ``` ### ** IMPORTANT ** #### Import RecoilizeDebugger from the Recoilize module ```js import RecoilizeDebugger from 'recoilize'; ``` #### Integrate RecoilizeDebugger as a React component within the recoil root: ```js import RecoilizeDebugger from 'recoilize'; import RecoilRoot from 'recoil'; ReactDOM.render( , document.getElementById('root'), ); ``` #### Please note, Recoilize assumes that the HTML element used to inject your React application has an ID of 'root'. If it does not the HTML element must be passed in as an attribute called 'root' to the RecoilizeDebugger component #### Example: ```js import RecoilizeDebugger from 'recoilize'; import RecoilRoot from 'recoil'; //If your app injects on an element with ID of 'app' const app = document.getElementById('app'); ReactDOM.render( , app, ); ``` ### In order to integrate Next.js applications with RecoilizeDebugger, follow the example below. ```js //If your application uses Next.js modify the _app.js as follows import dynamic from 'next/dynamic'; import { useEffect, useState } from 'react'; import { RecoilRoot } from 'recoil'; function MyApp({ Component, pageProps }) { const [root, setRoot] = useState(null) const RecoilizeDebugger = dynamic( () => { return import('recoilize'); }, { ssr: false} ); useEffect(() => { if (typeof window.document !== 'undefined') { setRoot(document.getElementById('__next')); } }, [root]); return ( <> ); } export default MyApp; ``` #### Open your application on the Chrome Browser and start debugging with Recoilize! ##### (Only supported with React applications using Recoil as state management)

New Features for Version 2.0.1

Support for Recoil 0.1.3

Recoilize now supports the most recent update to the Recoil library.

Ease of Use

Recoilize nolonger requires atoms and selectors or the root HTML element to be passed into the RecoilizeDebugger React component. Simply import RecoilizeDubugger and integrate it within your app's RecoilRoot component.

Support for Concurrent Mode

Additonal functionality has been added for apps that utilize React's Suspense component. If a Suspense component was used to suspend component renderings those components will display with a red border in the component graph. This indicates that a component was suspended during the render of the selected snapshot.

Performance Metrics

A new tab, 'Metrics', has been incorperated into the dev tool. In this tab the user will find two graphs which display component render times. The flame graph displays the time a component took to render itself, and all of its child components. The bar graph displays the individual render times of each component.

Features

Time Travel

As one of the key features of Recoilize, the tool enables users to jump to any previous snapshots. Pressing the jump button next to each of the snapshots will change the DOM by setting the state to that snapshot.

Visualizations

Users are able to view visualizations for their application's state by clicking individual snapshots. Recoilize provides component trees and graphs, as well as the state trees in JSON format.

Throttle

In the settings tab, users are able to set throttle (in milliseconds) for large scale applications or any applications that changes state rapidly. The default is set at 70ms.

State Persistence

Recoilize allows the users to persist their application's state through a refresh or reload. At this time, the user is able to view the previous states in the dev tool, but cannot time travel to the states before refresh.

Additional Features

We will continue updating Recoilize alongside Recoil's updates!

Contributors

Bren Yamaguchi @github @linkedin

Saejin Kang @github @linkedin

Jonathan Escamila @github @linkedin

Sean Smith @github @linkedin

Justin Choo @github @linkedin

Anthony Lin @github @linkedin

Spenser Schwartz @github @linkedin

Steven Nguyen @github @linkedin

Henry Taing @github @linkedin

Seungho Baek @github @linkedin

Aaron Yang @github @linkedin

Jesus Vargas @github @linkedin

Davide Molino @github @linkedin

Taven Shumaker @github @linkedin

Janis Hernandez @github @linkedin

Jaime Baik @github @linkedin

Anthony Magallanes @github @linkedin

Edward Shei @github @linkedin

Nathan Bargers @github @linkedin

Scott Campbell @github @linkedin

Steve Hong @github @linkedin

Jason Lee @github @linkedin

Razana Nisathar @github @linkedin

================================================ FILE: package/formatFiberNodes.js ================================================ // node parameter should be root of the fiber node tree, can be grapped with startNode from below // const startNode = document.getElementById('root')._reactRootContainer._internalRoot.current; const formatFiberNodes = node => { const formattedNode = { // this function grabs a 'name' based on the tag of the node name: assignName(node), tag: node.tag, children: [], recoilNodes: createAtomsSelectorArray(node), actualDuration: node.actualDuration, treeBaseDuration: node.treeBaseDuration, wasSuspended: node.return && node.return.tag === 13 ? true : false, }; // loop through and recursively call all nodes to format their 'sibling' and 'child' properties to our desired tree shape let currentNode = node.child; while (currentNode) { formattedNode.children.push(formatFiberNodes(currentNode)); currentNode = currentNode.sibling; } return formattedNode; }; const createAtomsSelectorArray = node => { // initialize empty array for all atoms and selectors. Elements will be all atom and selector names, as strings const recoilNodes = []; //start the pointer at node.memoizedState. All nodes should have this key. let currentNode = node.memoizedState; // Traverse through the memoizedStates and look for the deps key which holds selectors or state. while (currentNode) { // if the memoizedState has a deps key, and that deps key is an array // then the first value of that array will be an atom or selector if ( typeof(currentNode) === 'object' && currentNode.hasOwnProperty('memoizedState') && typeof currentNode.memoizedState === 'object' && currentNode.memoizedState !== null && !Array.isArray(currentNode.memoizedState) && currentNode.memoizedState.hasOwnProperty('deps') ) { if ( Array.isArray(currentNode.memoizedState.deps) && typeof currentNode.memoizedState.deps[0] === 'object' && currentNode.memoizedState.deps[0] !== null ) { // if recoilNodes (arr) includes the current atom or selector if (!recoilNodes.includes(currentNode.memoizedState.deps[0].key)) { // otherwise push atom/selector to recoilNodes recoilNodes.push(currentNode.memoizedState.deps[0].key); } } } // move onto next node currentNode = currentNode.next; } // return atom and selectors array return recoilNodes; }; // keep an eye on this section as we test bigger and bigger applications SEAN const assignName = node => { // Returns symbol key if $$typeof is defined. Some components, such as context providers, will have this value. if (node.type && node.type.$$typeof) return Symbol.keyFor(node.type.$$typeof); // Return suspense if tag is equal to 13, which is associated with Suspense components. if (node.tag === 13) return 'Suspense'; // Find name of a class component if (node.type && node.type.name) return node.type.name; // Tag 5 === HostComponent if (node.tag === 5) return `${node.type}`; // Tag 3 === HostRoot if (node.tag === 3) return 'HR'; // Tag 6 === HostText if (node.tag === 6) return node.memoizedProps; // Tag 7 === Fragment if (node.tag === 7) return 'Fragment'; }; module.exports = { formatFiberNodes }; // if testing this function on the browser, use line below to log the formatted tree in the console //let formattedFiberNodes = formatFiberNodes(document.getElementById('root')._reactRootContainer._internalRoot.current) ================================================ FILE: package/formatFiberNodes.ts ================================================ // node parameter should be root of the fiber node tree, can be grapped with startNode from below // const startNode = document.getElementById('root')._reactRootContainer._internalRoot.current; type node = { tag: number; key: any; elementType: string; child: any; sibling: any; actualDuration: number; treeBaseDuration: number; return: any; }; type formattedNode = { name: string; tag: number; children: any[]; recoilNodes: any[]; // adding in render time for fiber node actualDuration: number; treeBaseDuration: number; wasSuspended: boolean; }; const formatFiberNodes = (node: node) => { const formattedNode: formattedNode = { actualDuration: node.actualDuration, treeBaseDuration: node.treeBaseDuration, // this function grabs a 'name' based on the tag of the node name: assignName(node), tag: node.tag, children: [], recoilNodes: createAtomsSelectorArray(node), wasSuspended: node.return && node.return.tag === 13 ? true : false, }; // loop through and recursively call all nodes to format their 'sibling' and 'child' properties to our desired tree shape let currentNode = node.child; while (currentNode) { formattedNode.children.push(formatFiberNodes(currentNode)); currentNode = currentNode.sibling; } return formattedNode; }; const createAtomsSelectorArray = (node: any) => { // initialize empty array for all atoms and selectors. Elements will be all atom and selector names, as strings const recoilNodes = []; //start the pointer at node.memoizedState. All nodes should have this key. let currentNode = node.memoizedState; // Traverse through the memoizedStates and look for the deps key which holds selectors or state. while (currentNode) { // if the memoizedState has a deps key, and that deps key is an array of length 2 then the first value of that array will be an atom or selector if ( currentNode.deps && Array.isArray(currentNode.deps) && currentNode.deps.length === 2 ) { // if the atom/selector already exist in the recoilNodes array then break from this while loop. At this point you are traversing through previous atom/selector deps. if (recoilNodes.includes(currentNode.deps[0].key)) break; recoilNodes.push(currentNode.deps[0].key); // if an atom/selector was successfully pushed into the recoilNodes array then the pointer should now point to the next key, which will have its own deps key if there is another atom/selector currentNode = currentNode.next; } else { // This is the case where there is no atom/selector in the memoizedState. Look into the memoized state of the next key. If that doesn't exist then break from the while loop because there are no atoms/selectors at this point. if (!currentNode.next) break; if (!currentNode.next.memoizedState) break; currentNode = currentNode.next.memoizedState; } } return recoilNodes; }; // keep an eye on this section as we test bigger and bigger applications const assignName = (node: any) => { // Returns symbol key if $$typeof is defined. Some components, such as context providers, will have this value. if (node.type && node.type.$$typeof) return Symbol.keyFor(node.type.$$typeof); // Return suspense if tag is equal to 13, which is associated with Suspense components. if (node.tag === 13) return 'Suspense'; // Find name of a class component if (node.type && node.type.name) return node.type.name; // Tag 5 === HostComponent if (node.tag === 5) return `${node.type}`; // Tag 3 === HostRoot if (node.tag === 3) return 'HR'; // Tag 3 === HostText if (node.tag === 6) { return node.memoizedProps; } if (node.tag === 7) return 'Fragment'; }; export default formatFiberNodes; // if testing this function on the browser, use line below to log the formatted tree in the console //let formattedFiberNodes = formatFiberNodes(document.getElementById('root')._reactRootContainer._internalRoot.current) ================================================ FILE: package/index.js ================================================ import React, {useState, useEffect} from 'react'; import { useRecoilTransactionObserver_UNSTABLE, useRecoilSnapshot, useGotoRecoilSnapshot, useRecoilState, useGetRecoilValueInfo_UNSTABLE } from 'recoil'; import {formatFiberNodes} from './formatFiberNodes'; // grabs isPersistedState from sessionStorage let isPersistedState = sessionStorage.getItem('isPersistedState'); // isRestored state disables snapshots from being recorded // when we jump backwards let isRestoredState = false; // set default throttle to 70, throttle timer changes with every snapshot let throttleTimer = 0; let throttleLimit = 70; // assign the value of selectorsObject in formatRecoilizeSelectors function // will contain the selectors from a user application let selectorsObject; export default function RecoilizeDebugger(props) { // We should ask for Array of atoms and selectors. // Captures all atoms that were defined to get the initial state // Define a recoilizeRoot variable which will be assigned based on whether a root is passed in as a prop let recoilizeRoot; // Check if a root was passed to props. if (props.root) { const {root} = props; recoilizeRoot = root; } else { recoilizeRoot = document.getElementById('root'); } const snapshot = useRecoilSnapshot(); // getNodes_UNSTABLE will return an iterable that contains atom and selector objects. const nodes = [...snapshot.getNodes_UNSTABLE()]; // Local state of all previous snapshots to use for time traveling when requested by dev tools. const [snapshots, setSnapshots] = useState([snapshot]); // const [isRestoredState, setRestoredState] = useState(false); const gotoSnapshot = useGotoRecoilSnapshot(); const filteredSnapshot = {}; /* A nodeDeps object is constructed using getDeps_UNSTABLE. This object will then be used to construct a nodeSubscriptions object. After continuous testing, getSubscriptions_UNSTABLE was deemed too unreliable. */ const nodeDeps = {}; const nodeSubscriptions = {}; nodes.forEach(node => { const getDeps = [...snapshot.getInfo_UNSTABLE(node).deps]; nodeDeps[node.key] = getDeps.map(dep => dep.key); }); for (let key in nodeDeps) { nodeDeps[key].forEach(node => { if (nodeSubscriptions[node]) { nodeSubscriptions[node].push(key); } else { nodeSubscriptions[node] = [key]; } }); } // Traverse all atoms and selector state nodes and get value nodes.forEach((node, index) => { const type = node.__proto__.constructor.name; const contents = snapshot.getLoadable(node).contents; // Construct node data structure for dev tool to consume filteredSnapshot[node.key] = { type, contents, nodeDeps: nodeDeps[node.key], nodeToNodeSubscriptions: nodeSubscriptions[node.key] ? nodeSubscriptions[node.key] : [], }; }); // React lifecycle hook on re-render useEffect(() => { // Window listener for messages from dev tool UI & background.js window.addEventListener('message', onMessageReceived); if (!isRestoredState) { const devToolData = createDevToolDataObject(filteredSnapshot); // Post message to content script on every re-render of the developers application only if content script has started sendWindowMessage('recordSnapshot', devToolData); } else { isRestoredState = false; } // Clears the window event listener. return () => window.removeEventListener('message', onMessageReceived); }); // Listener callback for messages sent to windowf const onMessageReceived = msg => { // Add other actions from dev tool here switch (msg.data.action) { // Checks to see if content script has started before sending initial snapshot case 'contentScriptStarted': if (isPersistedState === 'false' || isPersistedState === null) { const initialFilteredSnapshot = formatAtomSelectorRelationship( filteredSnapshot, ); // once application renders, grab the array of atoms and array of selectors const appsKnownAtomsArray = [...snapshot._store.getState().knownAtoms] // console.log('Store State.getState: Atoms', appsKnownAtomsArray); const appsKnownSelectorsArray = [...snapshot._store.getState().knownSelectors] // console.log('Store State.getState: Selectors', appsKnownSelectorsArray); const atomsAndSelectorsMsg = { atoms: appsKnownAtomsArray, selectors: appsKnownSelectorsArray, $selectors: selectorsObject // the selectors object that contain key and set / get methods as strings } //creating a indexDiff variable //only created on initial creation of devToolData //determines difference in length of backend snapshots array and frontend snapshotHistoryLength to avoid off by one error const indexDiff = snapshots.length - 1; const devToolData = createDevToolDataObject( initialFilteredSnapshot, indexDiff, atomsAndSelectorsMsg, ); sendWindowMessage('moduleInitialized', devToolData); } else { setProperIndexForPersistedState(); sendWindowMessage('persistSnapshots', null); } break; // Listens for a request from dev tool to time travel to previous state of the app. case 'snapshotTimeTravel': timeTravelToSnapshot(msg); break; case 'persistState': switchPersistMode(); break; // Implementing the throttle change case 'throttleEdit': throttleLimit = parseInt(msg.data.payload.value); break; default: break; } }; // assigns or switches isPersistedState in sessionStorage const switchPersistMode = () => { if (isPersistedState === 'false' || isPersistedState === null) { // switch isPersistedState in sessionStorage to true sessionStorage.setItem('isPersistedState', true); // stores the length of current list of snapshots in sessionStorage sessionStorage.setItem('persistedSnapshots', snapshots.length); } else { // switch isPersistedState in sessionStorage to false sessionStorage.setItem('isPersistedState', false); } }; // function retreives length and fills snapshot array const setProperIndexForPersistedState = () => { const retreived = sessionStorage.getItem('persistedSnapshots'); const snapshotsArray = new Array(Number(retreived) + 1).fill({}); setSnapshots(snapshotsArray); }; // Sends window an action and payload message. const sendWindowMessage = (action, payload) => { window.postMessage( JSON.parse(JSON.stringify({ action, payload, })), '*', ); }; const createDevToolDataObject = (filteredSnapshot, diff, atomsAndSelectors) => { if (diff === undefined) { return { filteredSnapshot: filteredSnapshot, componentAtomTree: formatFiberNodes( recoilizeRoot._reactRootContainer._internalRoot.current, ), atomsAndSelectors, }; } else { return { filteredSnapshot: filteredSnapshot, componentAtomTree: formatFiberNodes( recoilizeRoot._reactRootContainer._internalRoot.current, ), indexDiff: diff, atomsAndSelectors, }; } }; const formatAtomSelectorRelationship = filteredSnapshot => { if ( window.$recoilDebugStates && Array.isArray(window.$recoilDebugStates) && window.$recoilDebugStates.length ) { let snapObj = window.$recoilDebugStates[window.$recoilDebugStates.length - 1]; if (snapObj.hasOwnProperty('nodeDeps')) { for (let [key, value] of snapObj.nodeDeps) { filteredSnapshot[key].nodeDeps = Array.from(value); } } if (snapObj.hasOwnProperty('nodeToNodeSubscriptions')) { for (let [key, value] of snapObj.nodeToNodeSubscriptions) { filteredSnapshot[key].nodeToNodeSubscriptions = Array.from(value); } } } return filteredSnapshot; }; // Will add hover effect over highlighted component // Takes an argument of msg.data which contains name and payload const activateHover = payload => { let name = payload.name; }; // FOR TIME TRAVEL: time travels to a given snapshot, re renders application. const timeTravelToSnapshot = async msg => { isRestoredState = true; await gotoSnapshot(snapshots[msg.data.payload.snapshotIndex]); }; // FOR TIME TRAVEL: Recoil hook to fire a callback on every atom/selector change -- research Throttle useRecoilTransactionObserver_UNSTABLE(({snapshot}) => { const now = new Date().getTime(); if (now - throttleTimer < throttleLimit) { isRestoredState = true; } else { throttleTimer = now; } if (!isRestoredState) { setSnapshots([...snapshots, snapshot]); } }); return null; } // function that receives objects to be passed into selector constructor function to post a message to the window // cannot send an object with a property that contains a function to the window - need to stringify the set and get methods export function formatRecoilizeSelectors(...selectors){ // create object to be sent via window message from target recoil application selectorsObject = {}; // iterate through our array of objects selectors.forEach(selector => { // check if the current selector object contains a set method, if so, reassign it to a stringified version if (selector.hasOwnProperty('set')){ selector.set = selector.set.toString(); } // check if the current selector object contains a get method, if so, reassign it to a stringified version if (selector.hasOwnProperty('get')){ selector.get = selector.get.toString(); } // store the selector in the payload object - providing its property name as the 'key' property of the current selector object // providing the object the property name of selector key will give easy searchability in GUI application for selector dropdown selectorsObject[selector.key] = selector; }); } ================================================ FILE: package/index.ts ================================================ import {useState, useEffect} from 'react'; import { useRecoilTransactionObserver_UNSTABLE, useRecoilSnapshot, useGotoRecoilSnapshot, } from 'recoil'; import formatFiberNodes from './formatFiberNodes'; // isRestored state disables snapshots from being recorded let isRestoredState = false; export default function RecoilizeDebugger(props: any) { // We should ask for Array of atoms and selectors. // Captures all atoms that were defined to get the initial state let nodes = null; if (typeof props.nodes === 'object' && !Array.isArray(props.nodes)) { nodes = Object.values(props.nodes); } else if (Array.isArray(props.nodes)) { nodes = props.nodes; } const {root} = props; const snapshot: any = useRecoilSnapshot(); // Local state of all previous snapshots to use for time traveling when requested by dev tools. const [snapshots, setSnapshots] = useState([snapshot]); // const [isRestoredState, setRestoredState] = useState(false); const gotoSnapshot = useGotoRecoilSnapshot(); const filteredSnapshot: any = {}; const currentTree = snapshot._store.getState().currentTree; // Traverse all atoms and selector state nodes and get value nodes.forEach((node: any) => { const type = node.__proto__.constructor.name; const contents = snapshot.getLoadable(node).contents; const nodeDeps = currentTree.nodeDeps.get(node.key); const nodeToNodeSubscriptions = currentTree.nodeToNodeSubscriptions.get( node.key, ); // Construct node data structure for dev tool to consume filteredSnapshot[node.key] = { type, contents, nodeDeps: nodeDeps ? Array.from(nodeDeps) : [], nodeToNodeSubscriptions: nodeToNodeSubscriptions ? Array.from(nodeToNodeSubscriptions) : [], }; }); // React lifecycle hook on re-render useEffect(() => { if (!isRestoredState) { setTimeout(() => { const devToolData = createDevToolDataObject(filteredSnapshot); // Post message to content script on every re-render of the developers application only if content script has started sendWindowMessage('recordSnapshot', devToolData); }, 0); } else { isRestoredState = false; } // Window listener for messages from dev tool UI & background.js window.addEventListener('message', onMessageReceived); // Clears the window event listener. return () => window.removeEventListener('message', onMessageReceived); }); // Listener callback for messages sent to window const onMessageReceived = (msg: any) => { // Add other actions from dev tool here switch (msg.data.action) { // Checks to see if content script has started before sending initial snapshot case 'contentScriptStarted': const initialFilteredSnapshot = formatAtomSelectorRelationship( filteredSnapshot, ); const devToolData = createDevToolDataObject(initialFilteredSnapshot); sendWindowMessage('moduleInitialized', devToolData); break; // Listens for a request from dev tool to time travel to previous state of the app. case 'snapshotTimeTravel': timeTravelToSnapshot(msg); break; default: break; } }; // Sends window an action and payload message. const sendWindowMessage = (action: any, payload: any) => { window.postMessage( { action, payload, }, '*', ); }; const createDevToolDataObject = (filteredSnapshot: any) => { return { filteredSnapshot: filteredSnapshot, componentAtomTree: formatFiberNodes( root._reactRootContainer._internalRoot.current, ), }; }; const formatAtomSelectorRelationship = (filteredSnapshot: any) => { const windowAny: any = window; if ( windowAny.$recoilDebugStates && Array.isArray(windowAny.$recoilDebugStates) && windowAny.$recoilDebugStates.length ) { let snapObj = windowAny.$recoilDebugStates[windowAny.$recoilDebugStates.length - 1]; if (snapObj.hasOwnProperty('nodeDeps')) { for (let [key, value] of snapObj.nodeDeps) { filteredSnapshot[key].nodeDeps = Array.from(value); } } if (snapObj.hasOwnProperty('nodeToNodeSubscriptions')) { for (let [key, value] of snapObj.nodeToNodeSubscriptions) { filteredSnapshot[key].nodeToNodeSubscriptions = Array.from(value); } } } return filteredSnapshot; }; // FOR TIME TRAVEL: time travels to a given snapshot, re renders application. const timeTravelToSnapshot = async (msg: any) => { isRestoredState = true; await gotoSnapshot(snapshots[msg.data.payload.snapshotIndex]); }; // FOR TIME TRAVEL: Recoil hook to fire a callback on every snapshot change useRecoilTransactionObserver_UNSTABLE(({snapshot}) => { if (!isRestoredState) { setSnapshots([...snapshots, snapshot]); } }); return null; } ================================================ FILE: package/package.json ================================================ { "name": "recoilize", "version": "1.0.0", "description": "Recoil Dev Tool", "main": "index.js", "repository": { "type": "git", "url": "https://github.com/open-source-labs/Recoilize" }, "peerDependencies": { "react": "^16.13.1", "react-dom": "^16.13.1", "recoil": "^0.1.2" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Bren Yamaguchi, Saejin Kang, Jonathan Escamilla, Sean Smith, Justin Choo, Anthony Lin, Spenser Schwartz, Steven Nguyen, Henry Taing, Seungho Baek, Taven Shumaker, Aaron Yang, Jesus Vargas, Davide Molino, Janis Hernandez, Jaime Baik, Anthony Magallanes, Edward Shei, Leonard Lew, Joey Ma, Harvey Nguyen, Victor Wang", "license": "MIT", "devDependencies": { "typescript": "^3.9.6" } } ================================================ FILE: package.json ================================================ { "name": "recoilize", "jest": { "setupFiles": [ "jest-webextension-mock" ] }, "version": "3.0.0", "description": "A Chrome extension that helps debug Recoil applications by memorizing the state of components with every render.", "main": "index.js", "scripts": { "build": "webpack --mode production", "dev": "webpack --mode development --watch", "test": "jest --verbose", "lint": "eslint '**/*.tsx' '**/*.ts' '**/*.js' --ignore-path .gitignore" }, "author": "Bren Yamaguchi, Saejin Kang, Jonathan Escamilla, Sean Smith, Justin Choo, Anthony Lin, Spenser Schwartz, Steven Nguyen, Henry Taing, Seungho Baek, Taven Shumaker, Aaron Yang, Jesus Vargas, Davide Molino, Janis Hernandez, Jaime Baik, Anthony Magallanes, Edward Shei, Leonard Lew, Joey Ma, Harvey Nguyen, Victor Wang", "license": "MIT", "devDependencies": { "@babel/core": "^7.10.3", "@babel/polyfill": "^7.10.4", "@babel/preset-env": "^7.10.3", "@babel/preset-react": "^7.10.1", "@babel/preset-typescript": "^7.10.4", "@testing-library/jest-dom": "^5.11.0", "@testing-library/react": "^10.4.6", "@types/react": "^16.9.41", "@types/react-dom": "^16.9.8", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "css-loader": "^3.6.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "eslint": "^7.4.0", "eslint-config-fbjs": "^3.1.1", "eslint-config-prettier": "^6.11.0", "eslint-plugin-babel": "^5.3.1", "eslint-plugin-flowtype": "^5.2.0", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.20.3", "jest": "^26.1.0", "jest-webextension-mock": "^3.6.1", "prettier": "^2.0.5", "recoil": "^0.7.2", "style-loader": "^2.0.0", "ts-loader": "^7.0.5", "typescript": "^3.9.6", "webpack": "^4.43.0", "webpack-chrome-extension-reloader": "^1.3.0", "webpack-cli": "^3.3.12" }, "dependencies": { "@angular/cli": "^10.0.5", "@angular/core": "^10.0.8", "@popperjs/core": "^2.4.4", "@reduxjs/toolkit": "^1.5.0", "@testing-library/react-hooks": "^3.4.1", "@types/chrome": "0.0.117", "@types/react-html-parser": "^2.0.1", "@types/react-json-tree": "^0.6.11", "@types/react-redux": "^7.1.16", "@vx/group": "0.0.196", "@vx/hierarchy": "0.0.196", "@vx/responsive": "0.0.196", "babel-eslint": "^10.1.0", "codemirror": "^5.65.2", "d3": "^5.16.0", "d3-hierarchy": "1.1.9", "d3-interpolate": "1.4.0", "d3-scale": "3.2.1", "d3-scale-chromatic": "1.5.0", "d3-shape": "1.3.7", "jest-webextension-mock": "^3.6.1", "jsondiffpatch": "^0.4.1", "multiselect-react-dropdown": "^1.5.7", "prop-types": "^15.7.2", "rc-slider": "^9.7.5", "rc-tooltip": "^5.1.1", "react": "^16.13.1", "react-codemirror2": "^7.2.1", "react-dom": "^16.13.1", "react-html-parser": "^2.0.2", "react-json-tree": "^0.11.2", "react-multi-select-component": "^3.0.1", "react-redux": "^7.2.3", "react-router-dom": "^5.2.0", "react-spring": "^8.0.27", "react-testing-library": "^8.0.1", "redux-persist": "^6.0.0", "rxjs": "^6.6.2", "semantic-ui-react": "^1.1.1", "style-loader": "^2.0.0" }, "engines": { "node": "^10.12.0 || >=12.0.0" } } ================================================ FILE: src/README.md ================================================ # Developer README ## Brief Though Recoil.js is still in the experimental state, it has proved its ability and catched a tremendous amount of attentions from experience developers for the past three years. We created Recoilize with one great mission in mind - providing a helpful tool so the developers can make an easy transition to Recoil.js and making the debugging process more effective. Recoilize is an open source product that is maintained and iterated constantly, and we're always welcome developers who are interested in it. Getting on board and understanding the codebase are never easy, so here are some useful tips and information that will help you get started quickly. ## File Structure The scr folder contains Recoilize's source code - frontend and chrome extension. ``` src/ ├── app/ # Frontend code │   ├── components/ # React components │   ├── Containers/ # More React components │   ├── state-management/ # Redux Toolkit Slices │   ├── utils/ # Helper Functions │   └── index.tsx # Starting point for root App component │ ├── types/ │   └── index.d.ts # │ ├── extension/ # Chrome Extension code │   ├── build/ # Destination for bundles and manifest.json (Chrome config file) │ │ # │   ├── background.js # Chrome Background Script │   └── contentScript.ts # Chrome Content Script └── ``` Here is an in-depth view of the app's components: ![FRONTEND DATA FLOW](../assets/Diagram.png) ## Diagramming If there's an update in the file structure, we suggest using [excalidraw](https://excalidraw.com/) ## Future Features and Possible Improvements - Optimizing Time Travel Algorithm. The Time Travel Algorithm is working perfectly. However, we believe that it can be better by implementing a better algorithm (or different approaches). Please note that there's no right or wrong approaches, and everyone is welcome to try new ideas. - UI/UX improvement. The UI/UX aspect definitely has room for improvement. Please feel free to play around with the app to get some ideas. For example, look at the component graph, atom network, graphs (flame, ranked, comparison). - Testing is an important aspect, and we believe in TDD (Test Driven Development). However, due to time constraint and the limitation of resources, Recoilize is missing the testing part. Since testing has so much potential to improve, we strongly recommend developers who love testing get started as soon as you get onboard. - Documentations - Documentations often get neglected since developers usually focus on writing code to improve features. However, at Recoilize, we believe that documenting is one of the most important tasks. Why? Because it's helpful not only for you, your teammates but it can also be valuable for future developers who interested in Recoilize. So if you love Recoilize, take good care of it by writing good documentations. - Containerization - Have you ever wondered why the app that you made ran perfectly on your computer but it couldn't run on other's computers. That is a common problem that software engineers often encounter. We can solve this by containerize our app, and `Docker` is a good candidate for this task. ================================================ FILE: src/app/Containers/ButtonsContainer.tsx ================================================ import React from 'react'; const ButtonsContainer = () => { const howToUseHandler = () => { window.open('https://github.com/open-source-labs/Recoilize', '_blank'); }; return (
); }; export default ButtonsContainer; ================================================ FILE: src/app/Containers/MainContainer.tsx ================================================ import React from 'react'; import SnapshotsContainer from './SnapshotContainer'; import VisualContainer from './VisualContainer'; import TravelContainer from './TravelContainer'; const MainContainer: React.FC = () => { return (
); }; export default MainContainer; ================================================ FILE: src/app/Containers/SnapshotContainer.tsx ================================================ import React, {useEffect, useState, useRef} from 'react'; import {selectFilterState} from '../state-management/slices/FilterSlice'; import {useAppSelector, useAppDispatch} from '../state-management/hooks'; import {setRenderIndex} from '../state-management/slices/SnapshotSlice'; const SnapshotsContainer: React.FC = () => { const dispatch = useAppDispatch(); const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const selected = useAppSelector(state => state.selected.selectedData); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const filterData = useAppSelector(selectFilterState); const snapshotEndRef = useRef(null); let snapshotHistoryLength = snapshotHistory.length; // useEffect to scroll bottom whenever snapshot history changes useEffect(() => { scrollToBottom(); }, [snapshotHistoryLength]); // using scrollInToView makes a smoother scroll const scrollToBottom = (): void => { snapshotEndRef.current.scrollIntoView({behavior: 'smooth'}); }; const snapshotDivs: JSX.Element[] = []; // iterate the same length of our snapshotHistory for (let i = 0; i < snapshotHistoryLength; i++) { // filterFunc will return false if there is no change to state const filterFunc = (): boolean => { // don't use the counter for this, not reliable if (i === 0) { return true; } // checks if the filteredSnapshot object at index i has a key (atom or selector) found in the selected array. This would indicate that there was a change to that state/selector because filter is an array of objects containing differences between snapshots. if (filterData[i]) { for (let key in filterData[i].filteredSnapshot) { for (let j = 0; j < selected.length; j++) { if (key === selected[j].name) { return true; } } } } return false; }; const x: boolean = filterFunc(); if (x === false) { continue; } // renderTime is set equal to the actualDuration. If i is zero then we are obtaining actualDuration from the very first snapshot in snapshotHistory. This is to avoid having undefined filter elements since there will be no difference between snapshot at the first instance. let renderTime: number = snapshotHistory[i].componentAtomTree.treeBaseDuration; // if (i === 0) { // renderTime = snapshotHistory[0].componentAtomTree.treeBaseDuration; // } // //Checks to see if the actualDuration within filter is an array. If it is an array then the 2nd value in the array is the new actualDuration. // else if (Array.isArray(filterData[i].componentAtomTree.actualDuration)) { // renderTime = filterData[i].componentAtomTree.treeBaseDuration[1]; // } else { // renderTime = filterData[i].componentAtomTree.treeBaseDuration; // } // Push a div container to snapshotDivs array only if there was a change to state. // The div container will contain renderTimes evaluated above. snapshotDivs.push(
{ dispatch(setRenderIndex(i)); }}> {/*
  • {i}
  • */}
  • {`${Math.round(renderTime * 100) / 100}ms`}
  • , ); } //indexDiff is used to ensure the index of filter matches the index of the snapshots array in the backend let indexDiff: number = 0; if (filterData[0] && filterData[0].indexDiff) { indexDiff = filterData[0].indexDiff; } // functionality to postMessage the selected snapshot index to background.js const timeTravelFunc = (index: number) => { // variable to store/reference connection const backgroundConnection = chrome.runtime.connect(); //const test = chrome.extension.getBackgroundPage(); // post the message with index in payload to the connection backgroundConnection.postMessage({ action: 'snapshotTimeTravel', tabId: chrome.devtools.inspectedWindow.tabId, payload: { snapshotIndex: index + indexDiff, }, }); }; function prevClr() { const snapshotListArr = document.querySelectorAll('.individualSnapshot'); for (let i = 0; i < snapshotListArr.length; i++) { let index = parseInt(snapshotListArr[i].id.match(/\d+/g)[0]); if (index < renderIndex) { snapshotListArr[i].parentNode.removeChild(snapshotListArr[i]); } else break; } } function fwrdClr() { const snapshotListArr = document.querySelectorAll('.individualSnapshot'); for (let i = snapshotListArr.length - 1; i >= 0; i--) { let index = parseInt(snapshotListArr[i].id.match(/\d+/g)[0]); if (index > renderIndex) { snapshotListArr[i].parentNode.removeChild(snapshotListArr[i]); } else break; } } // create a function to store current data to local storage const toLocalStorage = (data: any) => { for (let i = 0; i < data.length; i++) { console.log('trigger toLocalStorage'); const jsonData = JSON.stringify(data[i]); localStorage.setItem(`${i}`, jsonData); } }; return (
    Clear Snapshots
    Snapshots
    {snapshotDivs}
    ); }; export default SnapshotsContainer; ================================================ FILE: src/app/Containers/TravelContainer.tsx ================================================ import React from 'react'; import MainSlider from '../components/Slider/MainSlider'; import ButtonsContainer from './ButtonsContainer'; const TravelContainer: React.FC = () => { return (
    ); }; export default TravelContainer; ================================================ FILE: src/app/Containers/VisualContainer.tsx ================================================ import React, {useState} from 'react'; import {RecoilRoot} from 'recoil'; import Diff from '../components/StateDiff/Diff'; import NavBar from '../components/NavBar/NavBar'; import Metrics from '../components/Metrics/MetricsContainer'; import Tree from '../components/StateTree/Tree'; import Network from '../components/AtomNetwork/AtomNetwork'; import AtomComponentVisualContainer from '../components/ComponentGraph/AtomComponentContainer'; import Settings from '../components/Settings/SettingsContainer'; import Testing from '../components/Testing/TestingContainer'; type navTypes = { [tabName: string]: JSX.Element; }; // Renders Navbar and conditionally renders Diff, Visualizer, and Tree const VisualContainer: React.FC = () => { // object containing all conditional renders based on navBar const nav: navTypes = { // compare the diff of filteredPrevSnap and filteredCurSnap 'State Diff': , // render JSON tree of snapshot 'State Tree': , // tree visualizer of components showing atom/selector relationships 'Component Graph': , // atom and selector subscription relationship 'Atom Network': , // quotes not needed where name = component variable // individual snapshot visualizer Metrics: , // settings tab Settings: , // add a testing tab Testing: ( ), }; // array of all nav obj keys const tabsList: string[] = Object.keys(nav); // useState hook to update which component to render in the VisualContainer const [tab, setTab] = useState('State Diff'); // conditionally render based on value of nav[tab] return (
    {nav[tab]}
    ); }; export default VisualContainer; ================================================ FILE: src/app/Containers/__tests__/MainContainer.unit.test.js ================================================ import React, {useState} from 'react'; import ReactDOM from 'react-dom'; import {getQueriesForElement, getByText} from '@testing-library/dom'; import {render, fireEvent} from '@testing-library/react'; import MainContainer from '../MainContainer'; it('Main Container Renders', () => { window.HTMLElement.prototype.scrollIntoView = jest.fn(); const {getByPlaceholderText, debug} = render( , ); }); ================================================ FILE: src/app/Containers/__tests__/SnapshotContainer.unit.test.js ================================================ import React, {useState} from 'react'; import ReactDOM from 'react-dom'; import {getQueriesForElement, getByText} from '@testing-library/dom'; import {render, fireEvent} from '@testing-library/react'; import SnapshotContainer from '../SnapshotContainer'; it('Snapshot Container Renders', () => { window.HTMLElement.prototype.scrollIntoView = jest.fn(); const {getByPlaceholderText, debug} = render( , ); }); ================================================ FILE: src/app/Containers/__tests__/VisualContainer.unit.test.js ================================================ import React, {useState} from 'react'; import ReactDOM from 'react-dom'; import {getQueriesForElement, getByText} from '@testing-library/dom'; import {render, fireEvent} from '@testing-library/react'; import VisualContainer from '../VisualContainer'; it('Visual Container Renders', () => { window.HTMLElement.prototype.scrollIntoView = jest.fn(); const {getByPlaceholderText, debug} = render( , ); }); ================================================ FILE: src/app/components/App.tsx ================================================ import React, {useEffect} from 'react'; import MainContainer from '../Containers/MainContainer'; import {selectedTypes} from '../../types'; // importing the diff to find difference import {diff} from 'jsondiffpatch'; import {useAppSelector, useAppDispatch} from '../state-management/hooks'; import { setSnapshotHistory, setRenderIndex, setCleanComponentAtomTree, } from '../state-management/slices/SnapshotSlice'; import { addSelected, setSelected, } from '../state-management/slices/SelectedSlice'; import { updateFilter, selectFilterState, } from '../state-management/slices/FilterSlice'; import {setAtomsAndSelectors} from '../state-management/slices/AtomsAndSelectorsSlice'; const LOGO_URL = './assets/Recoilize-v2.png'; const App: React.FC = () => { const dispatch = useAppDispatch(); // useState hook to update the snapshotHistory array // array of snapshots const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const selected = useAppSelector(state => state.selected.selectedData); // selected will be an array with objects containing filteredSnapshot key names (the atoms and selectors) // ex: [{name: 'Atom1'}, {name: 'Atom2'}, {name: 'Selector1'}, ...] // const [selected, setSelected] = useState([]); // todo: Create algo that will clean up the big setSnapshothistory object, now and before // ! Setting up the selected const filterData = useAppSelector(selectFilterState); // Whenever snapshotHistory changes, useEffect will run, and selected will be updated useEffect(() => { // whenever snapshotHistory changes, update renderIndex dispatch(setRenderIndex(snapshotHistory.length - 1)); let last; if (snapshotHistory[renderIndex]) { last = snapshotHistory[renderIndex].filteredSnapshot; } // we must compare with the original for (let key in last) { if (!snapshotHistory[0].filteredSnapshot[key]) { // only push if the name doesn't already exist const check = () => { for (let i = 0; i < selected.length; i++) { // break if it exists if (selected[i].name === key) { return true; } } // does not exist return false; }; if (!check()) { // console.log('after Check'); dispatch(addSelected({name: key})); } } } }, [snapshotHistory]); // Only re-run the effect if snapshot history changes -- react hooks //Update cleanComponentAtomTree as Render Index changes useEffect(() => { if (snapshotHistory.length === 0) return; dispatch( setCleanComponentAtomTree(snapshotHistory[renderIndex].componentAtomTree), ); }, [renderIndex]); // useEffect for snapshotHistory useEffect(() => { // SETUP connection to bg script const backgroundConnection = chrome.runtime.connect(); // INITIALIZE connection to bg script backgroundConnection.postMessage({ action: 'devToolInitialized', tabId: chrome.devtools.inspectedWindow.tabId, }); // console.log( // 'here is the background connection post message IN APP', // backgroundConnection, // ); // LISTEN for messages FROM bg script backgroundConnection.onMessage.addListener(msg => { if (msg.action === 'recordSnapshot') { // ! sets the initial selected //console.log('should have our atoms and selectors: ', msg.payload); if (!msg.payload[1]) { // ensures we only set initially const arr: selectedTypes[] = []; for (let key in msg.payload[0].filteredSnapshot) { arr.push({name: key}); } // setSelected(arr); //console.log('arr in App.tsx send to setSelected', arr) dispatch(setSelected(arr)); } // console.log( // 'this is snapshotHistory', // msg.payload[msg.payload.length - 1], // ); dispatch(setSnapshotHistory(msg.payload[msg.payload.length - 1])); // update state with the atoms and selectors!!! dispatch(setAtomsAndSelectors(msg.payload[0].atomsAndSelectors)); //console.log('Payload: IS IT HERE??, ', msg.payload); // ! Setting the FILTER Array if (!msg.payload[1] && filterData.length === 0) { // todo: currently the filter does not work if recoilize is not open, we must change msg.payload to incorporate delta function in the backend dispatch(updateFilter(msg.payload)); } else { // push the difference between the objects const delta = diff( msg.payload[msg.payload.length - 2], msg.payload[msg.payload.length - 1], ); // only push if the snapshot length is chill if (filterData.length < msg.payload.length) { dispatch(updateFilter([delta])); } } } }); }, []); // Render main container if we have detected a recoil app with the recoilize module passing data const renderMainContainer: JSX.Element = ; // Render module not found message if snapHistory is null, this means we have not detected a recoil app with recoilize module installed properly const renderModuleNotFoundContainer: JSX.Element = (

    Supported only with Recoil apps with the Recoilize NPM module.
    Please follow the installation instructions at  Recoilize

    ); return (
    {snapshotHistory.length ? renderMainContainer : renderModuleNotFoundContainer}
    ); }; export default App; ================================================ FILE: src/app/components/AtomNetwork/AtomNetwork.tsx ================================================ import React from 'react'; import AtomNetworkLegend from './AtomNetworkLegend'; import AtomNetworkVisual from './AtomNetworkVisual'; //Create the container passing in the JSX props for each individual component const AtomNetwork: React.FC = () => { return (
    ); }; export default AtomNetwork; ================================================ FILE: src/app/components/AtomNetwork/AtomNetworkLegend.tsx ================================================ import React, {useState} from 'react'; import {useAppSelector, useAppDispatch} from '../../state-management/hooks'; import { setSearchValue } from '../../state-management/slices/AtomNetworkSlice'; const AtomNetworkLegend: React.FC = () => { const dispatch = useAppDispatch(); //Retrieve snapshotHistory State from Redux Store const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const filteredCurSnap = snapshotHistory[renderIndex].filteredSnapshot; // an array of atoms and selector sub const atomAndSelectorArr = Object.entries(filteredCurSnap); // array of atom names (what the drop down showAtomMenu is going to display) const [atomList] = useState(atomAndSelectorArr.filter(([isAtom, obj]: [string, any])=> !obj.nodeDeps.length ? isAtom : null)); // array of selectors (what the drop down showSelectorMenu is going to display) const [selectorList] = useState(atomAndSelectorArr.filter(([isSelector, obj]:[string, any]) => obj.nodeDeps.length ? isSelector : null)); //state hook for showing list of atoms const [showAtomMenu, setShowAtomMenu] = useState(false); //state hook for showing list of selectors const [showSelectorMenu, setShowSelectorMenu] = useState(false); const [atomButtonClicked, setAtomButtonClicked] = useState(false); const [selectorButtonClicked, setSelectorButtonClicked] = useState(false); // function to handle change in search bar. Sets searchValue state const handleChange = (e: any) => { dispatch(setSearchValue(e.target.value)); }; // handles clicking on Selector and Atom buttom to bring down // list of atoms or selects function openDropdown(e: React.MouseEvent) { // if user clicks on atom list button const target = e.target as Element; if(target.id === 'AtomP') { // check if selector list was previously open, if it is, close it if(showSelectorMenu) setShowSelectorMenu(false); // open atom list setShowAtomMenu(!showAtomMenu); // empty search box dispatch(setSearchValue('')); setAtomButtonClicked(true); setSelectorButtonClicked(false); } // if user clicks on selector list button else if(target.id === 'SelectorP') { // check if atom list was previously open, if it is, close it if(showAtomMenu) setShowAtomMenu(false); // show Selector list setShowSelectorMenu(!showSelectorMenu); // empty search box dispatch(setSearchValue('')); setSelectorButtonClicked(true); setAtomButtonClicked(false); } } return (
    {/* //change the visibility of the div depending the value of the state */}
    {/* conditional rendering of dropdowns depending on the value of the state */} {showAtomMenu &&
    {atomList.map(([atom, atomObj], i)=> { return (
    ) })}
    } {showSelectorMenu &&
    {selectorList.map(([selector, selectorObj], i) => { return (
    ) })}
    }
    ) } export default AtomNetworkLegend; ================================================ FILE: src/app/components/AtomNetwork/AtomNetworkVisual.tsx ================================================ import React, {useState, useEffect} from 'react'; import * as d3 from 'd3'; import makeRelationshipLinks from '../../utils/makeRelationshipLinks'; import {useAppSelector} from '../../state-management/hooks'; import {filteredSnapshot} from '../../../types'; const AtomNetworkVisual: React.FC = () => { //Retrieve snapshotHistory State from Redux Store const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const filteredCurSnap = snapshotHistory[renderIndex].filteredSnapshot; //Retrieve atomNetworkSearch State from Redux Store const searchValue = useAppSelector(state => state.atomNetwork.searchValue); useEffect(() => { // new filtered snap object to be constructed with search value const newFilteredCurSnap: any = {}; // filters filteredCurSnap object with atoms and selectors that includes are search value const filter = (filteredCurSnap: filteredSnapshot) => { for (let key in filteredCurSnap) { if (key.toLowerCase().includes(searchValue.toLowerCase())) { newFilteredCurSnap[key] = filteredCurSnap[key]; grabNodeToNodeSubscriptions(newFilteredCurSnap[key]); grabNodeDeps(newFilteredCurSnap[key]); } } }; // helper functions to recursively include searched atoms/selectors' subscriptions const grabNodeToNodeSubscriptions = (node: any) => { let nodeSubscriptionLength = node.nodeToNodeSubscriptions.length; if (nodeSubscriptionLength > 0) { for (let i = 0; i < nodeSubscriptionLength; i += 1) { let currSN = node.nodeToNodeSubscriptions[i]; newFilteredCurSnap[currSN] = filteredCurSnap[currSN]; grabNodeToNodeSubscriptions(filteredCurSnap[currSN]); } } }; const grabNodeDeps = (node: any) => { let nodeDepsLength = node.nodeDeps.length; if (nodeDepsLength > 0) { for (let i = 0; i < nodeDepsLength; i += 1) { let currDepNode = node.nodeDeps[i]; newFilteredCurSnap[currDepNode] = filteredCurSnap[currDepNode]; grabNodeDeps(filteredCurSnap[currDepNode]); } } }; // invoke filter to populate newFilteredCurSnap filter(filteredCurSnap); document.getElementById('networkCanvas').innerHTML = ''; let edgepaths: any; let edgelabels: any; const networkContainer = document.querySelector('.networkContainer'); // sets starting position of the atomNetwork graph. // const width = networkContainer.clientWidth; // const height = networkContainer.clientHeight; const width = 300; const height = 300; // snap will be newFilteredCurSnap if searchValue exists, if not original let snap: any = searchValue ? newFilteredCurSnap : filteredCurSnap; // TRANSFORM DATA INTO D3 SUPPORTED FORMAT FOR NETWORK GRAPH const networkData: any = makeRelationshipLinks(snap); //Create Disjoint Force-Directed Graph const chart = (data: any) => { const links = data.links.map((d: any) => Object.create(d)); const nodes = data.nodes.map((d: any) => { return Object.create(d); }); const simulation = d3 .forceSimulation(nodes) .force( 'link', d3.forceLink(links).id((d: any) => d.id), ) .force('charge', d3.forceManyBody()) .force('x', d3.forceX()) .force('y', d3.forceY()); const svg = d3 // .create('svg') .select('#networkCanvas') .attr('viewBox', [-width / 2, -height / 2, width, height]); const link = svg .append('g') .attr('stroke', '#999') .attr('stroke-opacity', 0.6) .selectAll('line') .data(links) .join('line') .attr('stroke-width', (d: any) => Math.sqrt(d.value)); const node = svg .append('g') .attr('stroke', '#fff') .attr('stroke-width', 1.5) .selectAll('circle') .data(nodes) .join('circle') .attr('r', 5) .style('fill', function (d: any, i: any) { return d.name === 'Atom' ? '#9580FF' : '#FF80BF'; }); node .append('title') .attr('dx', 12) // .attr('text-anchor', 'middle') .attr('dy', '.35em') .text((d: any) => d.label) .attr('stroke', 'white') .attr('stroke-width', 3); node .append('text') .attr('dx', 12) .attr('dy', '.35em') .text(function (d: any) { return d.name; }); simulation.on('tick', () => { link .attr('x1', (d: any) => d.source.x) .attr('y1', (d: any) => d.source.y) .attr('x2', (d: any) => d.target.x) .attr('y2', (d: any) => d.target.y); node.attr('cx', (d: any) => d.x).attr('cy', (d: any) => d.y); }); //Zoom functions function zoomActions() { svg.attr('transform', d3.event.transform); } var zoomHandler = d3.zoom().on('zoom', zoomActions); zoomHandler(svg); // allows the nodes to be draggable const dragDrop = d3 .drag() .on('start', (node: {fx: any; x: any; fy: any; y: any}) => { node.fx = node.x; node.fy = node.y; }) .on('drag', (node: {fx: any; fy: any}) => { simulation.alphaTarget(1).restart(); node.fx = d3.event.x; node.fy = d3.event.y; }) .on('end', (node: {fx: any; fy: any}) => { if (!d3.event.active) { simulation.alphaTarget(0); } node.fx = null; node.fy = null; }); node.call(dragDrop); simulation.alpha(1).restart(); return svg.node(); }; chart(networkData); }); //end of useEffect return (
    ); }; export default AtomNetworkVisual; ================================================ FILE: src/app/components/AtomNetwork/__tests__/Network.unit.test.js ================================================ import React from 'react'; import Network from '../AtomNetwork'; import {render, cleanup} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import '@babel/polyfill'; import {filteredCurSnapMock} from '../../../../../mock/snapshot.js'; afterEach(cleanup); it('renders & matches snapshot', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); it('loads and displays Network component', () => { const {getByTestId} = render(); expect(getByTestId('networkCanvas')).toBeTruthy(); }); it('if empty object props is passed into Network', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); it('if mock data object prop is passed into Network', () => { const {asFragment} = render( , ); expect(asFragment()).toMatchSnapshot(); }); ================================================ FILE: src/app/components/AtomNetwork/__tests__/__snapshots__/Network.unit.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`if empty object props is passed into Network 1`] = `

    ATOM

    SELECTOR

    `; exports[`if mock data object prop is passed into Network 1`] = `
    0 dummyAtom1 1 listState 2 listState2 3 selectorTest 4 stateLengths

    ATOM

    SELECTOR

    `; exports[`renders & matches snapshot 1`] = `

    ATOM

    SELECTOR

    `; ================================================ FILE: src/app/components/ComponentGraph/AtomComponentContainer.tsx ================================================ import React, {useState} from 'react'; import AtomComponentVisual from './AtomComponentVisual'; import {filteredSnapshot, atom, selector} from '../../../types'; import {useAppSelector} from '../../state-management/hooks'; const AtomComponentVisualContainer: React.FC = () => { //Retrieve State from store const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const cleanedComponentAtomTree = useAppSelector( state => state.snapshot.cleanComponentAtomTree, ); const filteredCurSnap: filteredSnapshot = snapshotHistory[renderIndex].filteredSnapshot; const componentAtomTree = snapshotHistory[renderIndex].componentAtomTree; // this will be the atom or selector from the AtomSelectorLegend that the user clicked on. an array with the ele at index 0 as the name of the atom/selector, and ele at index 1 will be 'atom' or 'selector' // Why was selectedRecoilValue formatted as an array? why not an object? const [selectedRecoilValue, setSelectedRecoilValue] = useState([]); const [str, setStr] = useState([]); // each property in the atoms or selectors object will be a property whose key is the atom or selector name, // and whose value is the value of that atom or selector const atoms: atom = {}; const selectors: selector = {}; if (filteredCurSnap) { for (let [recoilValueName, object] of Object.entries(filteredCurSnap)) { if (!object.nodeDeps.length) { atoms[recoilValueName] = object.contents; } else { selectors[recoilValueName] = object.contents; } } } return (
    ); }; export default AtomComponentVisualContainer; ================================================ FILE: src/app/components/ComponentGraph/AtomComponentVisual.tsx ================================================ import React, {useState, useEffect} from 'react'; import * as d3 from 'd3'; import {componentAtomTree, atom, selector} from '../../../types'; import {useAppSelector, useAppDispatch} from '../../state-management/hooks'; import { updateZoomState, selectZoomState, setDefaultZoom, } from '../../state-management/slices/ZoomSlice'; interface AtomComponentVisualProps { componentAtomTree: componentAtomTree; cleanedComponentAtomTree: componentAtomTree; selectedRecoilValue: string[]; atoms: atom; selectors: selector; setStr: React.Dispatch>; setSelectedRecoilValue: React.Dispatch>; } const AtomComponentVisual: React.FC = ({ componentAtomTree, cleanedComponentAtomTree, selectedRecoilValue, atoms, selectors, setStr, setSelectedRecoilValue, }) => { const zoomSelector = useAppSelector(selectZoomState); const {x, y, k} = zoomSelector; // initial scaling const dispatch = useAppDispatch(); // set the heights and width of the tree to be passed into treeMap function let width: number = 0; let height: number = 0; // useState hook to update the toggle of displaying entire tree or cleaned tree const [rawToggle, setRawToggle] = useState(false); // useState hook to update whether a suspense component will be shown on the component graph const [hasSuspense, setHasSuspense] = useState(false); //declare hooks to render lists of atoms or selectors const [atomList, setAtomList] = useState(Object.keys(atoms)); // returns an array of atom's properties const [selectorList, setSelectorList] = useState(Object.keys(selectors)); // need to create a hook for toggling const [showAtomMenu, setShowAtomMenu] = useState(false); const [showSelectorMenu, setShowSelectorMenu] = useState(false); // hook for selected button styles on the legend const [atomButtonClicked, setAtomButtonClicked] = useState(false); const [selectorButtonClicked, setSelectorButtonClicked] = useState( false, ); const [bothButtonClicked, setBothButtonClicked] = useState(false); const [isDropDownItem, setIsDropDownItem] = useState(false); // hooks for sibling level node spacing adjustment const [siblingSpacingFactor, setSiblingSpacingFactor] = useState(1); const [siblingSlider, setSiblingSlider] = useState(10); // hooks for parent-child node spacing adjustment const [parentSpacingFactor, setParentSpacingFactor] = useState(1); const [parentChildSlider, setParentChildSlider] = useState(10); // hook for component graph orientation const [vertOrient, setVertOrient] = useState(false); useEffect(() => { height = document.querySelector('.Component').clientHeight; width = document.querySelector('.Component').clientWidth; document.getElementById('canvas').innerHTML = ''; // reset hasSuspense to false. This will get updated to true if the red borders are rendered on the component graph. setHasSuspense(false); // creating the main svg container for d3 elements const svgContainer = d3.select('#canvas'); // creating a pseudo-class for reusability const g = svgContainer .append('g') .attr('transform', `translate(${x}, ${y}), scale(${k})`) .attr('id', 'componentGraph'); let i = 0; let duration: number = 750; let root: any; let path: string; // creating the tree map const treeMap = d3.tree().nodeSize( [ height * siblingSpacingFactor, width * parentSpacingFactor ] ); if (!rawToggle) { // expanded tree root = d3.hierarchy( cleanedComponentAtomTree, function (d: componentAtomTree) { return d.children; }, ); } else { // collapsed tree root = d3.hierarchy(componentAtomTree, function (d: componentAtomTree) { return d.children; }); } // Node distance from each other root.x0 = 10; root.y0 = width / 2; update(root); // d3 zoom functionality let zoom = d3.zoom().on('zoom', zoomed); // https://github.com/d3/d3-zoom/blob/v3.0.0/README.md#zoom_transform svgContainer.call( zoom.transform, // Changes the initial view, (left, top) d3.zoomIdentity.translate(x, y).scale(k), ); // allows the canvas to be zoom-able svgContainer.call( d3 .zoom() .scaleExtent([0.05, 0.9]) // [max zoomed out view, max zoomed in view] .on('zoom', zoomed), ); // helper function that allows for zooming function zoomed() { g.attr('transform', d3.event.transform).on( 'mouseup', dispatch( updateZoomState(d3.zoomTransform(d3.select('#canvas').node())), ), ); } // Update function /*** * Function: Update() * Parameters: source * Output: * */ function update(source: any) { treeMap(root); let nodes = root.descendants(), links = root.descendants().slice(1); let node = g .selectAll('g.node') .attr('stroke-width', 5) .data(nodes, function (d: any): number { return d.id || (d.id = ++i); }); /* getting group element with id = "componentGraph" ex 1: ex 2: PlaygroundRender https://devdocs.io/d3~5/d3-selection#selectAll */ /* this tells node where to be placed and go to * adding a mouseOver event handler to each node * display the data in the node on hover * add mouseOut event handler that removes the popup text */ //add div that will hold info regarding atoms and/or selectors for each node const tooltip = d3 .select('.tooltipContainer') .append('div') .attr('class', 'hoverInfo') .style('opacity', 0); let nodeEnter = node .enter() .append('g') .attr('class', 'node') .attr('transform', function (): string { return `translate(${source.y0}, ${source.x0})`; // getting && setting its "transform" attr to "translate(x, y) --> css manipulation" }) .on('click', click) .on('mouseover', function (d: any, i: number): void { // atsel is an array of all the atoms and selectors const atsel: any = []; if (d.data.recoilNodes) { for (let x = 0; x < d.data.recoilNodes.length; x++) { // pushing all the atoms and selectors for the node into 'atsel' atsel.push(d.data.recoilNodes[x]); } // change the opacity of the node when the mouse is over d3.select(this).transition().duration('50').attr('opacity', '.85'); // created a str for hover div to have corrensponding info // let newStr = formatAtomSelectorText(atsel).join('
    '); // newStr = newStr.replace(/,/g, '
    '); // newStr = newStr.replace(/{/g, '
    {'); const nodeData = formatAtomSelectorText(atsel)[0]; const genHTML = (obj: any): string => { let str = ''; let htmlStr = ''; for (let key in obj) { const curr = obj[key]; if (key === 'type') str += `${curr}: `; if (key === 'name') str += curr; if (key === 'info') { htmlStr += `

    ${str}

    `; htmlStr += `
    Atomic Values
    `; if (typeof curr === 'string') htmlStr += `

    title: ${curr}

    `; else for (let prop in curr) { const title = prop; const data = curr[prop]; htmlStr += `

    ${title}: ${data}

    `; } } } return `
    ${htmlStr}
    `; }; // tooltip appear near your mouse when hover over a node tooltip .style('opacity', 1) .html(genHTML(nodeData)) .style('left', d3.event.pageX + 15 + 'px') // mouse position .style('top', d3.event.pageY - 20 + 'px'); } }) .on('mouseout', function (d: any, i: number): void { d3.select(this).transition().duration('50').attr('opacity', '1'); tooltip.style('opacity', 0); }); // determines shape/color/size of node // adding styling attributes nodeEnter .append('circle') .attr('class', 'node') .attr('r', determineSize) .attr('fill', colorComponents) .style('stroke', borderColor) .style('stroke-width', 15); // TO DO: Add attribute for border if it is a suspense component // what is a suspense component and what color to add? // for each node that got created, append a text element that displays the name of the node nodeEnter .append('text') .attr('dy', '.31em') .attr('y', (d: any): number => (d.data.recoilNodes ? 138 : -75)) .attr('text-anchor', function (d: any): string { return d.children || d._children ? 'middle' : 'middle'; }) .text((d: any): string => d.data.name) .style('font-size', `7.5rem`) .style('fill', 'white'); let nodeUpdate = nodeEnter.merge(node); // transition that makes it slide down to next spot nodeUpdate // .transition() // .duration(duration) .attr('transform', function (d: any): string { return vertOrient ? `translate(${d.x}, ${d.y})` : `translate(${d.y}, ${d.x})`; }); // allows user to see hand pop out when clicking is available and maintains color/size nodeUpdate .select('circle.node') .attr('r', determineSize) .attr('fill', colorComponents) .attr('cursor', 'pointer') .style('stroke', borderColor) .style('stroke-width', 15); let nodeExit = node .exit() .transition() .duration(duration) .attr('transform', function (d: any): string { return `translate(${source.y}, ${source.x})`; }) .remove(); let link = g .attr('fill', 'none') .attr('stroke-width', 5) .selectAll('path.link') .data(links, function (d: any): number { return d.id; }); let linkEnter = link .enter() .insert('path', 'g') .attr('class', 'link') .attr('stroke', '#646464') .attr('stroke-width', 5) .attr('d', function (d: any): string { let o = {x: source.x0, y: source.y0}; return diagonal(o, o); }); let linkUpdate = linkEnter.merge(link); linkUpdate .transition() .duration(duration) .attr('stroke', '#646464') .attr('stroke-width', 5) .attr('d', function (d: any): string { return diagonal(d, d.parent); }); let linkExit = link .exit() .transition() .duration(duration) .attr('stroke', '#646464') .attr('stroke-width', 5) .attr('d', function (d: any): string { let o = {y: source.y, x: source.x}; return diagonal(o, o); }) .remove(); nodes.forEach(function (d: any): void { d.x0 = d.x; d.y0 = d.y; }); function diagonal(s: any, d: any): string { if (vertOrient) { path = `M ${s.x} ${s.y} C ${s.x} ${(s.y + d.y) / 2}, ${d.x} ${(s.y + d.y) / 2}, ${d.x} ${d.y}`; } else { path = `M ${s.y} ${s.x} C ${(s.y + d.y) / 2} ${s.x}, ${(s.y + d.y) / 2} ${d.x}, ${d.y} ${d.x}`; } return path; } function click(d: any): void { if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } update(d); const atsel = []; if (d.data.recoilNodes) { for (let x = 0; x < d.data.recoilNodes.length; x++) { atsel.push(d.data.recoilNodes[x]); } setStr(formatAtomSelectorText(atsel)); } } // allows the canvas to be draggable node.call(d3.drag()); // https://github.com/d3/d3-selection/blob/v3.0.0/README.md#selection_call function formatMouseoverXValue(recoilValue: string): number { if (atoms.hasOwnProperty(recoilValue)) { return -30; } return -150; } function formatAtomSelectorText(atomOrSelector: string[]): string[] { const recoilData: any = []; for (let i = 0; i < atomOrSelector.length; i++) { const data: any = {}; const curr = atomOrSelector[i]; data.type = atoms.hasOwnProperty(curr) ? 'atom' : 'selector'; data.name = curr; if (data.type === 'atom') { data.info = atoms[curr]; } else { data.info = selectors[curr]; } recoilData.push(data); } return recoilData; } function determineSize(d: any): number { if (d.data.recoilNodes && d.data.recoilNodes.length) { if (d.data.recoilNodes.includes(selectedRecoilValue[0])) { // Size when the atom/selector is clicked on from legend return 150; } // Size of atoms and selectors return 100; } // Size of regular nodes return 50; } function borderColor(d: any): string { if (d.data.wasSuspended) setHasSuspense(true); return d.data.wasSuspended ? '#FF0000' : 'none'; } function colorComponents(d: any): string { // if component node contains recoil atoms or selectors, make it orange red or yellow, otherwise keep node gray if (d.data.recoilNodes && d.data.recoilNodes.length) { if (d.data.recoilNodes.includes(selectedRecoilValue[0])) { // Color of atom or selector when clicked on in legend return 'yellow'; } let hasAtom = false; let hasSelector = false; for (let i = 0; i < d.data.recoilNodes.length; i++) { if (atoms.hasOwnProperty(d.data.recoilNodes[i])) { hasAtom = true; } if (selectors.hasOwnProperty(d.data.recoilNodes[i])) { hasSelector = true; } } if (hasAtom && hasSelector) { return 'springgreen'; } if (hasAtom) { return '#9580ff'; } else { return '#ff80bf'; } } return 'gray'; } } }, [componentAtomTree, rawToggle, siblingSpacingFactor, parentSpacingFactor, vertOrient, selectedRecoilValue]); // setting the component's user interface state by checking if the dropdown menu is open or not function openDropdown(e: React.MouseEvent) { const target = e.target as Element; // the button element with id 'AtomP' if (target.id === 'AtomP') { setAtomButtonClicked(true); setSelectorButtonClicked(false); setShowAtomMenu(!showAtomMenu); setShowSelectorMenu(false); } else { setAtomButtonClicked(false); setSelectorButtonClicked(true); setShowSelectorMenu(!showSelectorMenu); setShowAtomMenu(false); } } // resetting the component's user interface state when toggling between atoms & selectors const resetNodes = () => { setIsDropDownItem(false); setSelectedRecoilValue([]); setShowSelectorMenu(false); setShowAtomMenu(false); setAtomButtonClicked(false); setSelectorButtonClicked(false); }; return (
    { const val = parseInt(e.target.value); setSiblingSlider(val); setSiblingSpacingFactor(val / 10); }} />
    { const val = parseInt(e.target.value); setParentChildSlider(val); setParentSpacingFactor(val / 10); }} />
    {showAtomMenu && (
    {atomList.map((atom, i) => (
    ))}
    )}
    {showSelectorMenu && (
    {selectorList.map((selector, i) => (
    ))}
    )}

    {hasSuspense ? 'SUSPENSE' : ''}

    ); }; export default AtomComponentVisual; ================================================ FILE: src/app/components/ComponentGraph/__tests__/AtomComponentContainer.unit.test.js ================================================ import React from 'react'; import AtomComponentVisualContainer from '../AtomComponentContainer'; import {cleanup, render} from '@testing-library/react'; afterEach(cleanup); xit('testing to see if the component is properly rendered', () => { // This test gets 'TypeError: Cannot read property 'baseVal' of undefined // because Jest doesn't fully support testing SVGs yet const atomTree = {children: []}; const {component, debug} = render( , ); debug(); }); ================================================ FILE: src/app/components/ComponentGraph/__tests__/AtomComponentVisual.unit.test.js ================================================ import React from 'react'; import AtomComponentVisualContainer from '../AtomComponentContainer'; import AtomComponentVisual from '../AtomComponentVisual'; import {render, cleanup} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import '@babel/polyfill'; import { componentAtomTreeMock, filteredCurSnapMock, } from '../../../../../mock/snapshot'; afterEach(cleanup); xit('testing to see if the component is properly rendered', () => { // Now that we have the componentClassDiv appended, we get the same svg error as AtomComponentContainer test const componentClassDiv = document.createElement('div'); componentClassDiv.className = 'Component'; document.body.appendChild(componentClassDiv); // This test fails because it cannot find the element with class 'Component' const atomTree = {children: []}; const {component, debug} = render( // possibly test 'setStr', 'selectedRecoilValue', 'componentAtomTree' props , componentClassDiv, ); debug(); }); xit('renders & matches snapshot - no props', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); xit('renders & matches snapshot - componetAtomTree props', () => { const {asFragment} = render( , ); expect(asFragment()).toMatchSnapshot(); }); ================================================ FILE: src/app/components/ComponentGraph/__tests__/AtomSelectorLegend.unit.test.js ================================================ import React from 'react'; import AtomSelectorLegend from '../AtomSelectorLegend'; import {cleanup, render} from '@testing-library/react'; afterEach(cleanup); it('testing to see if the component is properly rendered', () => { const {component, debug} = render( // possibly test 'selectedRecoilValue', 'setSelectedRecoilValue', 'setStr' props , ); }); ================================================ FILE: src/app/components/Metrics/ComparisonGraph.tsx ================================================ import React, {useEffect, useRef} from 'react'; import * as d3 from 'd3'; import {dataDurationArr} from '../../../types'; import {useAppSelector} from '../../state-management/hooks'; interface ComparisonGraphProps { data: dataDurationArr; // an array of object{name:, actualDuration} width?: number; height?: number; } const ComparisonGraph: React.FC = ({ data, width, height, }: ComparisonGraphProps) => { const snapshotHistory = useAppSelector( (state: {snapshot: {snapshotHistory: any}}) => state.snapshot.snapshotHistory, ); console.log('comparison snapshot ', snapshotHistory); // declare an array that holds 2 objects: past and current const displayData = [ {name: 'past', duration: 0}, {name: 'current', duration: 0}, ]; // retrieve and get total duration for past serie from the local storage const values: any[] = []; const keys = Object.keys(localStorage); let i = keys.length; while (i--) { const series = localStorage.getItem(keys[i]); values.push(JSON.parse(series)); } for (const element of values) { displayData[0].duration += element.componentAtomTree.treeBaseDuration; } let total = 0; for (const element of snapshotHistory) { total += element.componentAtomTree.treeBaseDuration; } displayData[1].duration = total; // delete series in local storage const deleteSeries = () => { for (const i of keys) { localStorage.removeItem(i); } }; // svg const svgRef = useRef(); useEffect(() => { document.getElementById('canvas').innerHTML = ''; // set the dimensions and margins of the graph let left = 80; if (length > 13) { left = 100; } if (length > 17) { left = 120; } const margin = {top: 20, right: 20, bottom: 30, left}; // set range for y scale const y = d3.scaleBand().range([height, 0]).padding(0.2); // set range for x scale const x = d3.scaleLinear().range([0, width * 0.8]); // set range for durations const z = d3.scaleBand().range([height, 0]).padding(0.2); // determines the color based on actualDuration function colorPicker(data: any) { if (data <= 1) return '#51a8f0'; else if (data <= 2) return '#3a7bb0'; else return '#2d608a'; } // append the svg object to the body of the page // append a 'group' element to 'svg' // moves the 'group' element to the top left margin const svg = d3 .select(svgRef.current) .classed('svg-container', true) .append('svg') .attr('class', 'chart') .attr('viewBox', '-100 0 900 600') .attr('preserveAspectRatio', 'xMinYMin meet') .classed('svg-content-responsive', true) .call( d3.zoom().on('zoom', function () { svg.attr('transform', d3.event.transform); }), ) .append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); // Scale the range of the data in the domain x.domain([ 0, d3.max(displayData, (d: any) => { return d.duration; }), ]); // Scale the range of the data across the y-axis y.domain( displayData.map((d: any, i) => { return d.name + '-' + i; }), ); // Scale actualDuration with the y-axis z.domain( displayData.map((d: any, i) => { return d.duration.toFixed(2) + 'ms' + '-' + i; }), ); // append the rectangles for the bar chart svg .selectAll('.bar') .data(displayData) .enter() .append('rect') .attr('class', 'bar') .on('mouseover', function () { d3.select(this).attr('opacity', '0.85'); const backgroundConnection = chrome.runtime.connect(); const barName = this.data; const payload = { action: 'mouseover', tabId: chrome.devtools.inspectedWindow.tabId, payload: barName, }; backgroundConnection.postMessage(payload); }) // .transition() // .duration(1300) // .delay((d: any,i: any) => i * 100) .attr('width', function (d: any) { return x(d.duration); }) .attr('fill', function (d: any) { return colorPicker(d.duration); }) .attr('y', function (d: any, i: any) { return y(d.name + '-' + i); }) .attr('height', y.bandwidth()); // add x axis svg .append('g') .attr('transform', 'translate(0,' + height + ')') .call(d3.axisBottom(x)); // add y axis to able to have duplicate strings const yAxis = d3.axisLeft(y).tickFormat(function (d: any) { return d.split('-')[0]; }); yAxis(svg.append('g')); // add z axis to display all duration times const zAxis = d3.axisRight(z).tickFormat(function (d: any) { return d.split('-')[0]; }); zAxis(svg.append('g')); // svg.append('g') // .call(d3.axisRight(z)); }, [data]); return (
    ); }; export default ComparisonGraph; ================================================ FILE: src/app/components/Metrics/FlameGraph.js ================================================ import React, {useState, useRef} from 'react'; import {scaleLinear} from 'd3-scale'; import {interpolate} from 'd3-interpolate'; import {format as d3format} from 'd3-format'; import {hierarchy} from 'd3-hierarchy'; import {Group} from '@vx/group'; import {Partition} from '@vx/hierarchy'; import {useSpring, animated} from 'react-spring'; //determining the number of decimal places displayed const format = d3format('.2f'); const FlameGraph = ({cleanedComponentAtomTree, width, height}) => { //Building a heirarchy for d3 to graph const root = hierarchy(cleanedComponentAtomTree) //determining tree based duration by summing actual duration of children .sum(d => { // targets cleanedComponentAtomTree.actualDuration return d.actualDuration; }) //sorting children by their tree based duration for graph .sort((a, b) => b.value - a.value); // this is where we get value for App in this object //traversing tree to determine number of nodes let totalNodes = 0; root.each(() => { totalNodes = totalNodes + 1; return; }); //calculating average actualDuration of nodes in entire tree const averageDiration = root.value / totalNodes; //setting margins to fit graphed componets together and fit to container const margin = {top: 20, left: 0, right: 20, bottom: 30}; //scaleLinear outputs a funciton //this function is used to determine a graph components color based on actualDuration const color = scaleLinear() .domain([ averageDiration / 2, averageDiration * 3, averageDiration * 6, averageDiration * 8, ]) .range(['#ffffff', '#e9c7ff', '#ee9f30', '#ff0000']); //initiate graphArea as state variable area, and create setArea funciton const [area, setArea] = useState({ xDomain: [0, width], xRange: [0, width], yDomain: [0, height], yRange: [0, height], }); //define horizontal scaling of graph const xScale = useRef(scaleLinear().domain(area.xDomain).range(area.xRange)); //define vertical scaling of graph const yScale = useRef(scaleLinear().domain(area.yDomain).range(area.yRange)); //set interpolates to allow individual graph components to resize when entire graph resizes const xd = interpolate(xScale.current.domain(), area.xDomain); const yd = interpolate(yScale.current.domain(), area.yDomain); const yr = interpolate(yScale.current.range(), area.yRange); //set parameters for zooming animations const {t} = useSpring({ native: true, reset: true, from: {t: 0}, to: {t: 1}, config: { mass: 5, tension: 500, friction: 100, precision: 0.00001, }, onFrame: Param => { xScale.current.domain(xd(Param.t)); yScale.current.domain(yd(Param.t)).range(yr(Param.t)); }, }); //return an svg to render the FlameGraph return ( {data => ( {data.descendants().map((node, i) => ( `translate(${xScale.current(node.y0)}, ${yScale.current( node.x0, )})`, )} key={`node-${i}`} onClick={() => { if ( node.y0 === area.xDomain[0] && node.x0 === area.yDomain[0] && node.parent ) { // If the clicked graph component is already the selected componet, select parent setArea({ ...area, xDomain: [node.parent.y0, width], yDomain: [node.parent.x0, node.parent.x1], yRange: [0, height], }); // Otherwise select clicked } else { setArea({ ...area, xDomain: [node.y0, width], yDomain: [node.x0, node.x1], yRange: [0, height], }); } }}> xScale.current(node.y1) - xScale.current(node.y0), )} height={t.interpolate( () => yScale.current(node.x1) - yScale.current(node.x0), )} fill={color(node.data.actualDuration)} fillOpacity={1} /> {node.data.name} {' '} {format(node.data.actualDuration)} ))} )} ); }; export default FlameGraph; ================================================ FILE: src/app/components/Metrics/MetricsContainer.tsx ================================================ import React, {useState} from 'react'; import {ParentSize} from '@vx/responsive'; import {dataDurationArr} from '../../../types'; import FlameGraph from './FlameGraph.js'; import RankedGraph from './RankedGraph'; import ComparisonGraph from './ComparisonGraph'; import {useAppSelector} from '../../state-management/hooks'; const Metrics: React.FC = () => { const cleanedComponentAtomTree = useAppSelector( state => state.snapshot.cleanComponentAtomTree, ); //create state for the graph type toggle const [graphType, setGraphType] = useState('flame'); //funciton that toggles the graphType state const toggleGraphFunc = (check: string): void => { if (check === 'flame') setGraphType('flame'); if (check === 'ranked') setGraphType('ranked'); if (check === 'comparison') setGraphType('comparison'); }; // create an empty array to store objects for property name and actualDuration for rankedGraph const dataDurationArr: dataDurationArr = []; let length = 0; // function to traverse through the fiber tree const namesAndDurations = (node: any) => { if (node === undefined) return; if (node.name && node.actualDuration) { const obj: any = {}; if (node.name.length > length) { length = node.name.length; } obj['name'] = node.name; obj['actualDuration'] = node.actualDuration; dataDurationArr.push(obj); } node.children.forEach((child: any) => namesAndDurations(child)); }; namesAndDurations(cleanedComponentAtomTree); //function that renders either graphComponent based on graphType state variable const determineRender: any = () => { if (graphType === 'flame') { return ( //ParentSize component allows us to scale the FlameGraph to fit its container. {size => size.ref && ( ) } ); } else if (graphType === 'ranked') { return ( {size => size.ref && ( ) } ); } else if (graphType === 'comparison') { return ( {size => size.ref && ( ) } ); } }; //render the toggle buttons and the appropriate graph based on GraphType state variable return (
    {determineRender()}
    ); }; export default Metrics; ================================================ FILE: src/app/components/Metrics/RankedGraph.tsx ================================================ import React, {useEffect, useRef} from 'react'; import * as d3 from 'd3'; import {dataDurationArr} from '../../../types'; interface RankedGraphProps { data: dataDurationArr; // an array of object{name:, actualDuration} width?: number; height?: number; } const RankedGraph: React.FC = ({ data, width, height, }: RankedGraphProps) => { const svgRef = useRef(); useEffect(() => { document.getElementById('canvas').innerHTML = ''; // set the dimensions and margins of the graph let left = 80; if (length > 13) { left = 100; } if (length > 17) { left = 120; } const margin = {top: 20, right: 20, bottom: 30, left}; // set range for y scale const y = d3.scaleBand().range([height, 0]).padding(0.2); // set range for x scale const x = d3.scaleLinear().range([0, width * 0.8]); // set range for durations const z = d3.scaleBand().range([height, 0]).padding(0.2); // determines the color based on actualDuration function colorPicker(data: any) { if (data <= 1) return '#51a8f0'; else if (data <= 2) return '#3a7bb0'; else return '#2d608a'; } // append the svg object to the body of the page // append a 'group' element to 'svg' // moves the 'group' element to the top left margin const svg = d3 .select(svgRef.current) .classed('svg-container', true) .append('svg') .attr('class', 'chart') .attr('viewBox', '-100 0 900 600') .attr('preserveAspectRatio', 'xMinYMin meet') .classed('svg-content-responsive', true) .call( d3.zoom().on('zoom', function () { svg.attr('transform', d3.event.transform); }), ) .append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); // Scale the range of the data in the domain x.domain([ 0, d3.max(data, (d: any) => { return d.actualDuration; }), ]); // Scale the range of the data across the y-axis y.domain( data.map((d: any, i) => { return d.name + '-' + i; }), ); // Scale actualDuration with the y-axis z.domain( data.map((d: any, i) => { return d.actualDuration.toFixed(2) + 'ms' + '-' + i; }), ); // append the rectangles for the bar chart svg .selectAll('.bar') .data(data) .enter() .append('rect') .attr('class', 'bar') .on('mouseover', function () { d3.select(this).attr('opacity', '0.85'); const backgroundConnection = chrome.runtime.connect(); const barName = this.data; const payload = { action: 'mouseover', tabId: chrome.devtools.inspectedWindow.tabId, payload: barName, }; backgroundConnection.postMessage(payload); }) // .transition() // .duration(1300) // .delay((d: any,i: any) => i * 100) .attr('width', function (d: any) { return x(d.actualDuration); }) .attr('fill', function (d: any) { return colorPicker(d.actualDuration); }) .attr('y', function (d: any, i: any) { return y(d.name + '-' + i); }) .attr('height', y.bandwidth()); // add x axis svg .append('g') .attr('transform', 'translate(0,' + height + ')') .call(d3.axisBottom(x)); // add y axis to able to have duplicate strings const yAxis = d3.axisLeft(y).tickFormat(function (d: any) { return d.split('-')[0]; }); yAxis(svg.append('g')); // add z axis to display all duration times const zAxis = d3.axisRight(z).tickFormat(function (d: any) { return d.split('-')[0]; }); zAxis(svg.append('g')); // svg.append('g') // .call(d3.axisRight(z)); }, [data]); return (
    ); }; export default RankedGraph; ================================================ FILE: src/app/components/Metrics/__tests__/IcicleVerticle.unit.test.js ================================================ ================================================ FILE: src/app/components/Metrics/__tests__/Metrics.unit.test.js ================================================ import React from 'react'; import { render, cleanup, findByTestId } from '@testing-library/react'; import { componentAtomTree } from '../../../../types'; import { shallow } from 'enzyme'; import Metrics from '../Metrics'; // import prop object being passed down to metric graphs to be tested describe('Metrics graph testing', () => { afterEach(cleanup); //create a shallow copy of Metrics component passing in expected prop const wrapper = shallow(); describe('shape of props being passed down should match cleanedComponentAtomTree shape', () => { expect(wrapper.props().includedProp).toMatchObject(componentAtomTree); }); describe('component is being rendered correctly', () => { const { getByTestId } = render(); //if the component rendered then it has to have return an element with id 'canvas' //getBy returns an error when not finding an element, here is is looking for an id of canvas expect(getByTestId('canvas')).toBeTruthy(); }); }); ================================================ FILE: src/app/components/Metrics/__tests__/Visualizer.unit.test.js ================================================ import React from 'react'; import RankedGraph from '../RankedGraph'; import { filteredCurSnapMock, componentAtomTreeMock, } from '../../../../../mock/snapshot.js'; import {render, cleanup} from '@testing-library/react'; afterEach(cleanup); describe('Ranked graph displays correct information', () => { it('should display correct atom tree', () => { const {asFragment} = render( , ); expect(asFragment()).toMatchSnapshot(); }); //a component should display the render time of a component xit('render time should be of type number', () => { //the type of data being rendered should be a number expect().toBe(); }); xit('should be of type string', () => { //that dom element should render a string expect().toBe(); }); //a component should display the name of the component xit('should display correct name', () => { //that dom element should display the correct name expect().toBe(); }); }); describe('components rendering correctly', () => { it('renders without crashing', () => { const {getByTestId} = render(); expect(getByTestId('canvas')).toBeTruthy(); }); it('should match snapshot when props are passed into RankedGraph', () => { const {asFragment} = render( , ); expect(asFragment()).toMatchSnapshot(); }); it('should match snapshot when no props are passed in', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); xit('should render Recoil Root as text', () => { const {getByTestId} = render(); expect(getByTestId('canvas')).toHaveTextContent('Recoil Root'); }); }); ================================================ FILE: src/app/components/NavBar/NavBar.tsx ================================================ import React from 'react'; interface NavBarProps { // setState functionality to update tab setTab: React.Dispatch>; // array of object keys of conditional render object tabsList: string[]; // string of the current tab selected/rendered/displayed tab: string; } const NavBar: React.FC = ({setTab, tabsList, tab}) => { // array of buttons with setTab functionality const renderedTabButtons = tabsList.reduce((acc, tabName) => { acc.push( , ); return acc; }, []); // render the array of NavBar buttons generated above return
    {renderedTabButtons}
    ; }; export default NavBar; ================================================ FILE: src/app/components/NavBar/__test__/Navbar.unit.test.js ================================================ import React, {useState} from 'react'; import ReactDOM from 'react-dom'; import {getQueriesForElement, getByText} from '@testing-library/dom'; import {render, fireEvent} from '@testing-library/react'; import Navbar from '../Navbar'; it('renders to the dom', () => { const {debug} = render(); }); it('renders to the dom with the tab name', () => { const {getByText} = render( , ); getByText('testtab1'); }); ================================================ FILE: src/app/components/Settings/AtomSettings.tsx ================================================ import React, {useEffect} from 'react'; const {Multiselect} = require('multiselect-react-dropdown'); import {selectedTypes} from '../../../types'; import {useAppSelector, useAppDispatch} from '../../state-management/hooks'; import {setSelected} from '../../state-management/slices/SelectedSlice'; const AtomSettings: React.FC = () => { const dispatch = useAppDispatch(); //Retrieve snapshotHistory State from Redux Store const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const selected = useAppSelector(state => state.selected.selectedData); // https://github.com/srigar/multiselect-react-dropdown // Make filterArray into array of objects, we want to get the most recent so that we have all possible options const options: selectedTypes[] = []; // filling the options with the most recent for (let key in snapshotHistory[renderIndex].filteredSnapshot) { const obj: selectedTypes = {name: key}; options.push(obj); } // ! setting up the selected options const selected2: selectedTypes[] = []; for (let i = 0; i < selected.length; i++) { selected2.push({name: selected[i].name}); } // Todo: Create a conditional that will update the selected options onchange of the array -- updates if they are not equal, will add in NEW ADDITIONS // onSelect & onRemove functions for when selecting & removing atoms/selectors from the filter const onSelect = (selectedList: selectedTypes[]): void => { dispatch(setSelected(selectedList)); // propdrilled, so edited up top }; const onRemove = (selectedList: selectedTypes[]): void => { dispatch(setSelected(selectedList)); }; return (

    Atom and Selector Filter

    ); }; export default AtomSettings; ================================================ FILE: src/app/components/Settings/SettingsContainer.tsx ================================================ import React from 'react'; // Importing various settings components import ThrottleSettings from './ThrottleSettings'; import AtomSettings from './AtomSettings'; // renders the difference between the most recent state change and the previous const Settings: React.FC = () => { return (
    ); }; export default Settings; ================================================ FILE: src/app/components/Settings/ThrottleSettings.tsx ================================================ import React, {useState, useEffect} from 'react'; import {useAppSelector, useAppDispatch} from '../../state-management/hooks'; import { newThrottle, resetThrottle, } from '../../state-management/slices/ThrottleSlice'; const ThrottleSettings: React.FC = () => { const dispatch = useAppDispatch(); const throttle = useAppSelector(state => state.throttle.throttleValue); const [throttleNum, setThrottleNum] = useState(throttle); useEffect(() => { setThrottleNum(throttle); }, [throttle]); useEffect(() => { setThrottleNum(throttle); }, [throttle]); // onClick function for reset button. 70ms is the default throttle const onClick = (): void => { dispatch(resetThrottle()); }; // Creating function to tie to get to the backend const throttleFunc = (e: React.FormEvent): void => { e.preventDefault(); const backgroundConnection = chrome.runtime.connect(); // post the message with index in payload to the connection backgroundConnection.postMessage({ action: 'throttleEdit', tabId: chrome.devtools.inspectedWindow.tabId, payload: {value: throttleNum}, // edit this value to some other number }); dispatch(newThrottle(throttleNum)); }; return (

    Enter Throttle

    setThrottleNum(e.target.value)} value={throttleNum} placeholder="enter in milliseconds" />{' '} milliseconds
    Current throttle is{' '} {throttle === '70' ? `${throttle} (default)` : throttle} ms
    ); }; export default ThrottleSettings; ================================================ FILE: src/app/components/Settings/__tests__/AtomSettings.unit.test.js ================================================ import React, {useState} from 'react'; import ReactDOM from 'react-dom'; import {getQueriesForElement, getByText} from '@testing-library/dom'; import {render, fireEvent} from '@testing-library/react'; const {Multiselect} = require('multiselect-react-dropdown'); import AtomSettings from '../AtomSettings'; const mockprops = { snapshotHistory: [{}], selected: [{name: 'testname1'}, {name: 'testname2'}, {name: 'testname3'}], setSelected: jest.fn(), }; describe('atom settings properly rendering', () => { it('Component Renders', () => { const {getByPlaceholderText, debug, getByText} = render( , ); getByText('testname1'); }); // Check if render without crashing it('renders Atom Settings without crashing', () => { const root = document.createElement('div'); ReactDOM.render(, root); }); it('renders Multiselect component without crashing', () => { const root = document.createElement('div'); ReactDOM.render(, root); }); }) ================================================ FILE: src/app/components/Settings/__tests__/StateSettings.unit.test.js ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import StateSettings from '../StateSettings'; import {render, cleanup, getByTestId, fireEvent} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; afterEach(cleanup); const mockProps = { checked: false, setChecked: jest.fn(), }; it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render(, div); }); it('renders persist state text correctly', () => { const {getByTestId} = render(); expect(getByTestId('stateSettings')).toHaveTextContent('Persist State'); }); describe('input checkbox', () => { beforeAll(cleanup); afterAll(cleanup); it('is rendered correctly', () => { const {getByTestId} = render(); expect(getByTestId('stateSettingsToggle')).toHaveAttribute( 'type', 'checkbox', ); }); it('is unchecked initially', () => { const {getByTestId} = render(); const toggle = getByTestId('stateSettingsToggle'); expect(toggle).toHaveProperty('checked', false); }); it('is checked after user clicks', () => { const {getByTestId} = render(); const toggle = getByTestId('stateSettingsToggle'); chrome.runtime.connect = function () { return {postMessage: jest.fn()}; }; chrome.devtools = {inspectedWindow: {}}; fireEvent.change(toggle, {target: {checked: true}}); expect(toggle).toBeChecked(); }); }); it('should match snapshot when no props are passed in', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); it('should match snapshot when props are passed in', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); ================================================ FILE: src/app/components/Settings/__tests__/ThrottleSettings.unit.test.js ================================================ import React, {useState} from 'react'; import ReactDOM from 'react-dom'; import {getQueriesForElement, getByText} from '@testing-library/dom'; import {render, fireEvent} from '@testing-library/react'; import ThrottleSettings from '../ThrottleSettings'; // Check the render describe('Throttle Component renders correctly', () => { beforeEach(() => { const {} = render(); }); it('renders without crashing', () => { const root = document.createElement('div'); ReactDOM.render(, root); }); it('renders to the dom with the correct content', () => { const root = document.createElement('div'); ReactDOM.render(, root); const {getByText} = getQueriesForElement(root); expect(getByText('Enter Throttle')).not.toBeNull(); expect(getByText('Enter')).not.toBeNull(); expect(getByText('Reset')).not.toBeNull(); expect(getByText('Current throttle is 1000 ms')).not.toBeNull(); }); }); describe('Throttle Snapshots Testing', () => { it('renders & matches snapshots', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); }); describe('Check button and user input functionalities', () => { it('Check the user input typing', () => { const {getByPlaceholderText} = render( , ); const input = getByPlaceholderText('enter in milliseconds'); fireEvent.change(input, {target: {value: 500}}); // this successfully changes the value expect(input.value).toEqual('500'); }); it('Check the enter and reset button', () => { // creating mock functions for chrome chrome.runtime.connect = function () { return {postMessage: function () {}}; }; let setThrottleDisplay = jest.fn(); chrome.devtools = {inspectedWindow: {}}; // Rendering the component const {getByText} = render( , ); // Testing the buttons const submitButton = getByText('Enter'); // check these buttons -- how to test const resetButton = getByText('Reset'); fireEvent.click(submitButton); fireEvent.click(resetButton); }); }); ================================================ FILE: src/app/components/Settings/__tests__/__snapshots__/StateSettings.unit.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should match snapshot when no props are passed in 1`] = `

    Persist State

    `; exports[`should match snapshot when props are passed in 1`] = `

    Persist State

    `; ================================================ FILE: src/app/components/Settings/__tests__/__snapshots__/ThrottleSettings.unit.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Throttle Snapshots Testing renders & matches snapshots 1`] = `

    Enter Throttle

    milliseconds
    Current throttle is ms
    `; ================================================ FILE: src/app/components/Slider/MainSlider.tsx ================================================ import React from 'react'; import Slider from 'rc-slider'; import Tooltip from 'rc-tooltip'; import {useAppSelector, useAppDispatch} from '../../state-management/hooks'; import {setRenderIndex} from '../../state-management/slices/SnapshotSlice'; import {selectFilterState} from '../../state-management/slices/FilterSlice'; const {Handle} = Slider; interface handleProps { className: string; prefixCls?: string; vertical?: boolean; offset: number; value: number; dragging?: boolean; disabled?: boolean; min?: number; max?: number; reverse?: boolean; index: number; tabIndex?: number; } const handle = (props: handleProps) => { const {value, dragging, index, ...restProps} = props; return ( ); }; // interface MainSliderProps { // snapshotsLength: number; // } function MainSlider() { const dispatch = useAppDispatch(); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const filterData = useAppSelector(selectFilterState); console.log('this is renderIndex ', renderIndex); console.log('this is snapshotHistory length ', snapshotHistory.length); //indexDiff is used to ensure the index of filter matches the index of the snapshots array in the backend let indexDiff: number = 0; if (filterData[0] && filterData[0].indexDiff) { indexDiff = filterData[0].indexDiff; } const timeTravelFunc = (index: number) => { // variable to store/reference connection const backgroundConnection = chrome.runtime.connect(); //const test = chrome.extension.getBackgroundPage(); // post the message with index in payload to the connection backgroundConnection.postMessage({ action: 'snapshotTimeTravel', tabId: chrome.devtools.inspectedWindow.tabId, payload: { snapshotIndex: index + indexDiff, }, }); }; const forwardButton = () => { console.log('forward'); if (renderIndex < snapshotHistory.length - 1) { dispatch(setRenderIndex(renderIndex + 1)); timeTravelFunc(renderIndex + 1); } }; const backwardButton = () => { if (renderIndex > 0) { dispatch(setRenderIndex(renderIndex - 1)); timeTravelFunc(renderIndex - 1); } }; const playButton = () => { let currentIndex = 0; dispatch(setRenderIndex(currentIndex)); timeTravelFunc(currentIndex); const intervalId = setInterval(() => { if (currentIndex < snapshotHistory.length - 1) { dispatch(setRenderIndex(currentIndex + 1)); timeTravelFunc(currentIndex + 1); currentIndex += 1; } else { clearInterval(intervalId); } }, 1000); }; return (
    { dispatch(setRenderIndex(index)); timeTravelFunc(index); }} handle={handle} />
    ); } export default MainSlider; ================================================ FILE: src/app/components/SnapshotList/__tests__/SnapshotList.unit.test.js ================================================ import React, {useState} from 'react'; import {configure, shallow} from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import SnapshotsList from '../SnapshotList'; // Newer enzyme versions require this configuration to adapt to a particular version of React configure({adapter: new Adapter()}); describe('Testing for rendering of SnapshotList under several expected conditions', () => { let wrapper; let childObj; let props; beforeEach(() => { childObj = { actualDuration: 14, children: [], name: 'Replace', recoildNodes: [], tag: 0, treeBaseDuration: 6, wasSuspended: false, }; props = { renderIndex: 0, snapshotHistoryLength: 1, setRenderIndex: jest.fn(), timeTravelFunc: jest.fn(), selected: [{name: 'id'}], filter: [], snapshotHistory: [ { componentAtomTree: { actualDuration: 12, children: [ { ...childObj, name: 'RecoilRoot', childdren: [ { ...childObj, tag: 10, children: [{...childObj}], }, ], }, ], name: 'HR', recoilNodes: [], tag: 3, treeBaseDuration: 6, wasSuspended: false, }, filteredSnapshot: { id: { contents: 1, nodeDeps: [], nodeToNodeSubscriptions: [], type: 'RecoilState', }, }, }, ], }; }); it('Renders with no componentAtomTree children', () => { props.snapshotHistory[0].componentAtomTree.children = []; wrapper = shallow(); expect(wrapper.type()).toEqual('div'); expect(wrapper.find('div').first().hasClass('SnapshotsList')).toEqual(true); }); it('Renders one div with class individualSnapshot when filter is empty', () => { wrapper = shallow(); let individual = 0; for (let key of wrapper.find('div')) { if (key.props) { if (key.props.className) { if (key.props.className === 'individualSnapshot') individual += 1; } } } expect(individual).toBe(1); }); // Test to see if invalid props break the function. Functional component should render empty div when props are invalid. describe('Snapshots List Error Handling', () => { it('Snapshots List renders empty divs when renderIndex is invalid', () => { props.renderIndex = -1; wrapper = shallow(); expect(wrapper.type()).toEqual('div'); expect(wrapper.find('div').first().hasClass('SnapshotsList')).toEqual( true, ); }); it('Handles the snapshotHistoryLength prop being greater than the filter length', () => { props.snapshotHistoryLength = 8; wrapper = shallow(); expect(wrapper.type()).toEqual('div'); expect(wrapper.find('div').first().hasClass('SnapshotsList')).toEqual( true, ); }); }); }); ================================================ FILE: src/app/components/StateDiff/Diff.tsx ================================================ import React, {useState} from 'react'; import {diff, formatters} from 'jsondiffpatch'; import ReactHtmlParser from 'react-html-parser'; import {useAppSelector} from '../../state-management/hooks'; // renders the difference between the most recent state change and the previous const Diff: React.FC = () => { // retrieve snapshotHistory State from Redux Store const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const currentState = useAppSelector(state => state); console.log('newState in Diff: ', currentState); const filteredPrevSnap = renderIndex > 0 ? snapshotHistory[renderIndex - 1].filteredSnapshot : undefined; const filteredCurSnap = snapshotHistory[renderIndex] ? snapshotHistory[renderIndex].filteredSnapshot : snapshotHistory[0].filteredSnapshot; // useState hook to update the toggle of showUnchanged or hideUnchanged const [rawToggle, setRawToggle] = useState(false); // diffing between filteredPrevSnap && filteredCurSnap const delta = diff(filteredPrevSnap, filteredCurSnap); // string of html with comparisons const html = formatters.html.format(delta, filteredPrevSnap); // conditionally render changes or not based on rawToggle bool formatters.html.showUnchanged(rawToggle); return (
    {ReactHtmlParser(html)}
    ); }; export default Diff; ================================================ FILE: src/app/components/StateDiff/__tests__/StateDiff.unit.test.js ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import Diff from '../Diff'; import { render, cleanup, getByText, fireEvent, debug, } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { filteredPrevSnapMock, filteredCurSnapMock, } from '../../../../../mock/snapshot.js'; afterEach(cleanup); const mockProps = { filteredPrevSnap: filteredPrevSnapMock, filteredCurSnap: filteredCurSnapMock, }; describe('Diff Component', () => { it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render(, div); }); }); describe('Raw Toggle Button', () => { beforeAll(cleanup); afterAll(cleanup); it('raw button should be color #989898 at initial render', () => { const {getByText} = render(); expect(getByText('Raw')).toHaveStyle('color: #989898'); }); it('raw button should change to color #E6E6E6 after clicked', () => { const {getByText} = render(); fireEvent.click(getByText('Raw')); expect(getByText('Raw')).toHaveStyle('color: #E6E6E6'); }); }); it('should match snapshot when no props are passed in', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); it('should match snapshot when props are passed in', () => { const {asFragment} = render(); expect(asFragment()).toMatchSnapshot(); }); ================================================ FILE: src/app/components/StateDiff/__tests__/__snapshots__/StateDiff.unit.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should match snapshot when no props are passed in 1`] = `
    `; exports[`should match snapshot when props are passed in 1`] = `
    • dummyAtom1
                    {
        "contents": {
          "hello": [],
          "hi": []
        },
        "nodeDeps": [],
        "nodeToNodeSubscriptions": [],
        "type": "RecoilState"
      }
                  
    • listState
                    {
        "contents": [
          {
            "text": "list item"
          },
          {
            "text": "list item"
          },
          {
            "text": "list item"
          }
        ],
        "nodeDeps": [],
        "nodeToNodeSubscriptions": [
          "selectorTest",
          "stateLengths"
        ],
        "type": "RecoilState"
      }
                  
    • listState2
      • contents
        • 0
                                {
            "text": "list item"
          }
                              
        • 1
                                {
            "text": "list item"
          }
                              
        • 2
                                {
            "text": "list item"
          }
                              
        • 3
                                {
            "text": "list item"
          }
                              
      • nodeDeps
                          []
                        
      • nodeToNodeSubscriptions
                          [
          "stateLengths"
        ]
                        
      • type
                          "RecoilState"
                        
    • selectorTest
                    {
        "contents": "test",
        "nodeDeps": [
          "listState"
        ],
        "nodeToNodeSubscriptions": [],
        "type": "RecoilValueReadOnly"
      }
                  
    • stateLengths
                    {
        "contents": 6,
        "nodeDeps": [
          "listState",
          "listState2"
        ],
        "nodeToNodeSubscriptions": [],
        "type": "RecoilValueReadOnly"
      }
                  
    `; ================================================ FILE: src/app/components/StateTree/Tree.tsx ================================================ import React from 'react'; import JSONTree from 'react-json-tree'; import {useAppSelector} from '../../state-management/hooks'; const Tree: React.FC = () => { // render json tree while passing in newSnap as data to JSONTree //Retrieve snapshotHistory State from Redux Store const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const renderIndex = useAppSelector(state => state.snapshot.renderIndex); const filteredCurSnap = snapshotHistory[renderIndex].filteredSnapshot; return (
    {filteredCurSnap && ( ({className: 'json-tree'})}} shouldExpandNode={() => true} labelRenderer={raw => typeof raw[0] !== 'number' ? ( {raw[0]} ) : null } /> )}
    ); }; export default Tree; ================================================ FILE: src/app/components/StateTree/__tests__/Tree.unit.test.js ================================================ import React, {useState} from 'react'; import ReactDOM from 'react-dom'; import {getQueriesForElement, getByText} from '@testing-library/dom'; import {render, fireEvent} from '@testing-library/react'; import Tree from '../Tree'; it('Current Tree Renders', () => { const {getByPlaceholderText, debug} = render(); }); ================================================ FILE: src/app/components/Testing/CodeResults.js ================================================ /* eslint-disable prettier/prettier */ import React from 'react'; const CodeResults = props => { const { evaluatedCode } = props; return (
    {evaluatedCode}
    ) } export default CodeResults; ================================================ FILE: src/app/components/Testing/Editor.js ================================================ /* eslint-disable prettier/prettier */ import React from 'react'; import {useState} from 'react'; //import our css stylings from codemirror import 'codemirror/lib/codemirror.css'; import 'codemirror/theme/dracula.css'; import 'codemirror/mode/javascript/javascript'; import 'codemirror/mode/css/css'; import { Controlled as ControlledEditor } from 'react-codemirror2'; import CodeResults from './CodeResults' import { useSetRecoilState, useRecoilValue } from 'recoil'; import { selector, atom } from 'recoil'; import { dummySelector, dummyAtom } from './dummySelector'; const Editor = props => { const { value, onChange, loadButton, selectorsFnAsStrings, madeAtoms, toBeValue, currentAtom, currentAtomValue, currentSelector, setCurrentSelector, madeSelectors, parameters } = props; // expect two more prop drilling variables: expect (atom's current value) amd user inputted to be (atom's expected value) const [hasRendered, setHasRendered] = useState(false); let mySelector; let comparisonValue; // create the selectors and atoms for use in this editor associated with the drop down selection if (madeSelectors[currentSelector]){ mySelector = useSetRecoilState(madeSelectors[currentSelector]); if (!hasRendered){ mySelector() setHasRendered(true); } comparisonValue = useRecoilValue(madeAtoms[currentAtom]); } else { mySelector = useSetRecoilState(dummySelector); comparisonValue = useRecoilValue(dummyAtom); } const [ evaluatedCode, setEvaluatedCode ] = useState('Run code here...'); function handleRunCodeClick () { try { if (toBeValue !== comparisonValue) { setEvaluatedCode(`❌ Expected ${currentAtom} to be ${toBeValue} and Received ${comparisonValue}`); } else { setEvaluatedCode(`✅ Expected ${currentAtom} to be ${toBeValue} and Received ${comparisonValue}`); } } catch(err) { setEvaluatedCode(err.message); } } function handleChange (editor, data, value) { onChange(value); } return (
    ) } export default Editor; ================================================ FILE: src/app/components/Testing/SelectorsButton.tsx ================================================ /* eslint-disable prettier/prettier */ import React, {useState, useEffect} from 'react'; import DisplayTests from './displayTests'; import {useAppSelector} from '../../state-management/hooks'; import { useSetRecoilState } from 'recoil'; const SelectorsButton: React.FC = props => { const { selectorsFnAsStrings, selectors, atoms, onChange, currentSelector, setCurrentSelector, currentAtom, setCurrentAtom, currentAtomValue, setCurrentAtomValue, toBeValue, setToBeValue, parameters, setParameters, loadedSelector, setLoadedSelector, madeSelectors } = props; // create a hook that stores the current value of the selected drop down //const [currentSelector, setCurrentSelector] = useState(''); // label of the atom associated with the selcetor clicked from the drop down // value of the atom associated with the selector clicked from the drop down // value to be expected -> updated in displayTests // stateful value to contain parameters initialized as an empty array // grab the filtered snapshot so we know which atoms and selectors are dependent of each other const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const handleChange = (item) => { const selectorKey = item.options[item.selectedIndex].value; // update state with the chosen Selector setCurrentSelector(selectorKey); const capturedFnString = selectorsFnAsStrings[selectorKey]; let { key, set, get } = capturedFnString; const parser = (string) => { if (!string) return; // the portion before the fat arrow (parameters) const firstPortion = string.slice(0, string.indexOf(';')); // the portion after the fat arrow (function definition) const secondPortion = string.slice(string.indexOf(';') + 1, string.length); // determine if the passed in string has a get, set, or both get and set methods let newFirstPortion = ''; if (firstPortion.includes('get') && firstPortion.includes('set')) {newFirstPortion += 'get, set'} else if (firstPortion.includes('get')) newFirstPortion += 'get'; else if (firstPortion.includes('set')) newFirstPortion += 'set'; //parameter portion will be assigned the value of the strings following _ref6 (let {get,set}) //if there is a comma found within the slice between the parameter parenthesis let parameterPortion = ''; if (string.slice(string.indexOf('('), string.indexOf(')')).includes(',')) { parameterPortion = string.slice(string.indexOf(' ') + 1, string.indexOf(')')); // return the first portion ({ get and/or set }), the parameters, and the associated function definition return `({ ${newFirstPortion} }, ${parameterPortion}) => { ${secondPortion}`; } // return the first portion ({ get and/or set }) and the associated function definition return `({ ${newFirstPortion} }) => { ${secondPortion}`; } //first portion of string is from 0 to ; const displayedSelector = `Chosen selector: ${key}: { get: ${parser(get)}, set: ${parser(set)}, }` onChange(displayedSelector); // console.log('Selector Key: ', selectorKey); // setCurrentSelector(selectorKey); // find the current atom dependent on the selector clicked from the drop down // currently referencing the last element in the snapshotHistory array const dependentAtom = snapshotHistory[snapshotHistory.length - 1].filteredSnapshot[selectorKey].nodeDeps[0]; setCurrentAtom(dependentAtom); // find the current atom value from the dependentAtom associated with the clicked on Selector const dependentAtomValue = snapshotHistory[snapshotHistory.length - 1].filteredSnapshot[dependentAtom].contents; setCurrentAtomValue(dependentAtomValue); //setLoadedSelector(useSetRecoilState(madeSelectors.nextPlayerSetSelector)); //console.log('loaded Selector set: ', loadedSelector); } //relabeled and used a value property to capture the value on an on change above - you can now find the keys. Function needs to be completed though const HTMLselectorArray: JSX.Element[] = []; selectors.forEach((selector, i) => { HTMLselectorArray.push(); }); return (
    ); }; export default SelectorsButton; ================================================ FILE: src/app/components/Testing/TestingContainer.tsx ================================================ /* eslint-disable prettier/prettier */ import React, { useState, useEffect } from 'react'; import Editor from './Editor'; import SelectorsButton from './SelectorsButton'; import {useAppSelector} from '../../state-management/hooks'; import {selectAtomsAndSelectorsState} from '../../state-management/slices/AtomsAndSelectorsSlice'; import './testing.css'; import {atom, selector} from 'recoil'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; const Testing = () => { // retrieve snapshotHistory State from Redux Store const snapshotHistory = useAppSelector( state => state.snapshot.snapshotHistory, ); const [ currentAtom, setCurrentAtom ] = useState(''); const [ currentAtomValue, setCurrentAtomValue ] = useState(''); const [ toBeValue, setToBeValue ] = useState(''); const [ parameters, setParameters ] = useState(''); const [ loadButton, setLoadButton ] = useState(true); const [ secondLoadButton, setSecondLoadButton ] = useState(true); const [ atoms, setAtoms ] = useState([]); const [ selectors, setSelectors ] = useState([]); const [ selectorsFnAsStrings, setSelectorsFnAsStrings ] = useState({}); const [ madeSelectors, setMadeSelectors ] = useState({}); const [ madeAtoms, setMadeAtoms ] = useState({}); const theObject = JSON.parse(JSON.stringify(useAppSelector(selectAtomsAndSelectorsState))); const handleLoadClick = () => { setSelectorsFnAsStrings(theObject.atomsAndSelectors.$selectors); setAtoms(theObject.atomsAndSelectors.atoms); setSelectors(theObject.atomsAndSelectors.selectors); // create our atoms using recoil on the first render instance only const createdAtoms = {}; snapshotHistory[snapshotHistory.length - 1].atomsAndSelectors.atoms.forEach(theAtom => { createdAtoms[theAtom] = atom({ key: theAtom, default: snapshotHistory[snapshotHistory.length - 1].filteredSnapshot[theAtom].contents, }); }); setMadeAtoms(createdAtoms); setLoadButton(false); } const handleSecondClick = () => { let selectorsClone = JSON.parse(JSON.stringify(theObject.atomsAndSelectors.$selectors)); const createdSelectors = {}; theObject.atomsAndSelectors.selectors.forEach(selectorKey => { if (selectorsClone[selectorKey].set) { selectorsClone[selectorKey].set = selectorsClone[selectorKey].set.replaceAll('get(', 'get(madeAtoms.'); selectorsClone[selectorKey].set = selectorsClone[selectorKey].set.replaceAll('set(', 'set(madeAtoms.'); selectorsClone[selectorKey].set = eval('(' + selectorsClone[selectorKey].set + ')'); } if (selectorsClone[selectorKey].get){ selectorsClone[selectorKey].get = selectorsClone[selectorKey].get.replaceAll('get(', 'get(madeAtoms.'); selectorsClone[selectorKey].get = eval('(' + selectorsClone[selectorKey].get + ')'); } else { selectorsClone[selectorKey].get = ({ get }) => {return}; } createdSelectors[selectorKey] = selector(selectorsClone[selectorKey]); }); setMadeSelectors(createdSelectors); setSecondLoadButton(false); } // chosen selector piece of state that tells our container which piece of state has been chosen, and therefore will be drilled down (chosenSelector is just a string) const [ currentSelector, setCurrentSelector ] = useState(''); const [ loadedSelector, setLoadedSelector ] = useState(() => {return}); const [ javascript, setJavascript ] = useState(''); if (loadButton){ return (
    ); } else if (secondLoadButton){ return (
    ) } else { return ( //invoking an onclick to test out the fact that our selector works and is using the selector that WE MADE from our object.
    {/* requires a parameter to be passed in, regardless of whether or not it's used. */}

    Testing Window

    will need to grab our atom's value with matching key from our recoil state to compare with toBeValue currentAtomValue={currentAtomValue} // reassign our GUIs stateful atom as the value of currentAtomValue currentSelector={currentSelector} // the currentSelector chosen from our drop down menu (just the key) setCurrentSelector={setCurrentSelector} madeSelectors={madeSelectors} // the object containing our actual selectors from our recoil state -> match the key from currentSelector from our made selector with useSetRecoilState parameters={parameters} // pass down the parameters into editor to use />
    ) } }; export default Testing; ================================================ FILE: src/app/components/Testing/displayTests.tsx ================================================ /* eslint-disable prettier/prettier */ import React, {useState, useEffect} from 'react'; import {useRecoilState, useRecoilValue, useSetRecoilState} from 'recoil'; const DisplayTests: React.FC = (props) => { const {currentSelector, currentAtom, currentAtomValue, toBeValue, setToBeValue, parameters, setParameters} = props; // use displayedSelector to check if a new selector is chose // if truthy, reassign parameters and toBeValue to empty strings // reassign displayedSelector to the current value of if (currentSelector.length){ // update the toBe value with wahtever to function handleToBeChange(e) { setToBeValue(e.target.value); }; function handleParameterChange(e) { setParameters(e.target.value); console.log('E.TARGET.VAL: ', e.target.value); } return (

    Atom ({currentAtom}): {currentAtomValue}

    Selector: {currentSelector}

    expect({currentAtom}).toBe({toBeValue})

    ); } else { return (
    ); } }; export default DisplayTests; ================================================ FILE: src/app/components/Testing/dummySelector.js ================================================ import {atom, selector} from 'recoil'; export const dummySelector = selector({ key: 'dummySelector', get: ({ get }) => {return}, set: ({ set }) => {return} }); export const dummyAtom = atom({ key: 'dummyAtom', default: 'I am not a real atom' }) ================================================ FILE: src/app/components/Testing/testing.css ================================================ .testing-container { display: flex; flex-direction: column; margin: 5px; height: 100%; width: 100%; } .code-mirror-wrapper { font-size: medium; position: static; margin-top: 10px; margin-bottom: 10px; height: 75%; width: 95%; } .run-code { margin-top: 10px; margin-bottom: 10px; padding: 5px; } .console-output { font-size: medium; } ================================================ FILE: src/app/index.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/App'; import {store, persistor} from './state-management/index'; import {Provider} from 'react-redux'; import {PersistGate} from 'redux-persist/integration/react'; ReactDOM.render( , document.getElementById('root'), ); ================================================ FILE: src/app/state-management/__tests__/slices.test.tsx ================================================ import {setSearchValue} from '../slices/AtomNetworkSlice'; import {updateFilter} from '../slices/FilterSlice'; import {setSelected, addSelected} from '../slices/SelectedSlice'; import { setSnapshotHistory, setRenderIndex, setCleanComponentAtomTree, } from '../slices/SnapshotSlice'; import {newThrottle, resetThrottle} from '../slices/ThrottleSlice'; import {setDefaultZoom, updateZoomState} from '../slices/ZoomSlice'; import {store} from '../index'; import {snapshotHistoryMock} from '../../../../mock/state-snapshot'; describe('ZoomSlice', () => { it('update zoom state', () => { const obj: any = { x: 20, y: 530, k: 0.15, }; store.dispatch(updateZoomState(obj)); const newZoomState = store.getState().zoom.zoomData; expect(newZoomState.x).toEqual(20); expect(newZoomState.y).toEqual(530); expect(newZoomState.k).toEqual(0.15); }); it('reset zoom state', () => { store.dispatch(setDefaultZoom()); const defaultZoomState = store.getState().zoom.zoomData; expect(defaultZoomState.x).toEqual(18); expect(defaultZoomState.y).toEqual(527); expect(defaultZoomState.k).toEqual(0.12); }); }); describe('AtomNetworkSlice', () => { it('update search value', () => { store.dispatch(setSearchValue('square-8')); const newSearchValue = store.getState().atomNetwork.searchValue; expect(newSearchValue).toEqual('square-8'); }); }); describe('ThrottleSlice', () => { it('update throttle', () => { store.dispatch(newThrottle('100')); const newThrottleValue = store.getState().throttle.throttleValue; expect(newThrottleValue).toEqual('100'); }); it('reset throttle', () => { store.dispatch(resetThrottle()); const defaultThrottleValue = store.getState().throttle.throttleValue; expect(defaultThrottleValue).toEqual('70'); }); }); describe('FilterSlice', () => { it('update filter', () => { store.dispatch(updateFilter(['square-8'])); const filterData = store.getState().filter.filterData; expect(filterData).toEqual(['square-8']); }); }); describe('SnapshotSlice', () => { it('set render index', () => { store.dispatch(setRenderIndex(10)); const renderIndex = store.getState().snapshot.renderIndex; expect(renderIndex).toEqual(10); }); it('set cleaned component atom tree', () => { store.dispatch( setCleanComponentAtomTree( snapshotHistoryMock.snapshotHistory[0].componentAtomTree, ), ); const cleanedComponentAtomTreeData = store.getState().snapshot .cleanComponentAtomTree; const result: any = { children: [ { actualDuration: 0.8450000004813774, children: [], name: 'PlaygroundStart', recoilNodes: [], tag: 0, treeBaseDuration: 0.5550000005314359, wasSuspended: false, }, ], name: 'PlaygroundRender', recoilNodes: ['playStart'], tag: 0, actualDuration: 1.6750000004321919, }; expect(cleanedComponentAtomTreeData).toEqual(result); }); it('set snapshot', () => { store.dispatch(setSnapshotHistory(snapshotHistoryMock)); const snapshotData = store.getState().snapshot.snapshotHistory; expect(snapshotData).toEqual([snapshotHistoryMock]); }); }); describe('SelectedSlice', () => { beforeEach(() => { store.dispatch(setSelected([])); }); it('set selected item', () => { store.dispatch(setSelected(['square-8'])); const selectedData = store.getState().selected.selectedData; expect(selectedData).toEqual(['square-8']); }); it('add selected item', () => { store.dispatch(addSelected('square-8')); const selectedData = store.getState().selected.selectedData; expect(selectedData).toEqual(['square-8']); }); }); ================================================ FILE: src/app/state-management/hooks.tsx ================================================ import {TypedUseSelectorHook, useDispatch, useSelector} from 'react-redux'; import type {RootState, AppDispatch} from './index'; export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; ================================================ FILE: src/app/state-management/index.tsx ================================================ import {configureStore, getDefaultMiddleware} from '@reduxjs/toolkit'; import throttleReducer from './slices/ThrottleSlice'; import zoomReducer from '../state-management/slices/ZoomSlice'; import snapshotReducer from '../state-management/slices/SnapshotSlice'; import atomNetworkReducer from '../state-management/slices/AtomNetworkSlice'; import filterReducer from '../state-management/slices/FilterSlice'; import selectedReducer from './slices/SelectedSlice'; import atomsAndSelectorsReducer from './slices/AtomsAndSelectorsSlice'; import {persistStore, persistReducer} from 'redux-persist'; import storage from 'redux-persist/lib/storage/session'; import {combineReducers} from 'redux'; const customizedPayloadAction = getDefaultMiddleware({ serializableCheck: false, }); const reducers = combineReducers({ zoom: zoomReducer, throttle: throttleReducer, snapshot: snapshotReducer, atomNetwork: atomNetworkReducer, filter: filterReducer, selected: selectedReducer, atomsAndSelectors: atomsAndSelectorsReducer, }); const persistConfig = { key: 'root', storage, }; const persistedReducer = persistReducer(persistConfig, reducers); export const store = configureStore({ reducer: persistedReducer, middleware: customizedPayloadAction, }); export let persistor = persistStore(store); export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; ================================================ FILE: src/app/state-management/slices/AtomNetworkSlice.tsx ================================================ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; const initialState: any = { searchValue: '', }; export const atomNetworkSlice = createSlice({ name: 'atomNetwork', initialState, reducers: { setSearchValue: (state, action: PayloadAction) => { state.searchValue = action.payload; }, }, }); export const {setSearchValue} = atomNetworkSlice.actions; export default atomNetworkSlice.reducer; ================================================ FILE: src/app/state-management/slices/AtomsAndSelectorsSlice.tsx ================================================ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; import {RootState} from '../index'; interface selectedState { atomsAndSelectors: { atoms: string[]; selectors: string[]; $selectors: any; }; } const initialState: selectedState = { atomsAndSelectors: { atoms: [], selectors: [], $selectors: {}, }, }; // zoom: zoomReducer, // throttle: throttleReducer, // snapshot: snapshotReducer, // atomNetwork: atomNetworkReducer, // filter: filterReducer, // selected: selectedReducer, export const atomsAndSelectorsSlice = createSlice({ name: 'atomsAndSelectors', initialState, reducers: { setAtomsAndSelectors: (state, action: PayloadAction) => { state.atomsAndSelectors = action.payload; }, }, }); console.log('atomsAndSelectorsSlice:', atomsAndSelectorsSlice); export const {setAtomsAndSelectors} = atomsAndSelectorsSlice.actions; export const selectAtomsAndSelectorsState = (state: RootState) => state.atomsAndSelectors; export default atomsAndSelectorsSlice.reducer; ================================================ FILE: src/app/state-management/slices/FilterSlice.tsx ================================================ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; import {RootState} from '../index'; interface FilterState { filterData: any; } const initialState: FilterState = { filterData: [], }; export const filterSlice = createSlice({ name: 'filter', initialState, reducers: { updateFilter: (state, action: PayloadAction) => { state.filterData = [...state.filterData, ...action.payload]; }, }, }); export const {updateFilter} = filterSlice.actions; export const selectFilterState = (state: RootState) => state.filter.filterData; const filterReducer = filterSlice.reducer; export default filterReducer; ================================================ FILE: src/app/state-management/slices/SelectedSlice.tsx ================================================ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; import {selectedTypes} from '../../../types'; interface selectedState { selectedData: selectedTypes[]; } const initialState: selectedState = { selectedData: [], }; export const selectedSlice = createSlice({ name: 'selected', initialState, reducers: { setSelected: (state, action: PayloadAction) => { state.selectedData = action.payload; }, addSelected: (state, action: PayloadAction) => { state.selectedData.push(action.payload); }, }, }); export const {setSelected, addSelected} = selectedSlice.actions; // export const selectedState = (state: RootState) => state.selected.selectedData; const selectedReducer = selectedSlice.reducer; export default selectedReducer; ================================================ FILE: src/app/state-management/slices/SnapshotSlice.tsx ================================================ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; import generateCleanComponentAtomTree from '../../utils/cleanComponentAtomTree'; const initialState: any = { snapshotHistory: [], renderIndex: 0, cleanComponentAtomTree: {}, }; export const snapshotSlice = createSlice({ name: 'snapshotHistory', initialState, reducers: { setSnapshotHistory: (state, action: PayloadAction) => { state.snapshotHistory.push(action.payload); }, setRenderIndex: (state, action: PayloadAction) => { state.renderIndex = action.payload; }, setCleanComponentAtomTree: (state, action: PayloadAction) => { state.cleanComponentAtomTree = generateCleanComponentAtomTree( action.payload, ); }, }, }); console.log( 'what snapshotSlice looks like in SnapshotSlice.tsx: ', snapshotSlice, ); export const {setSnapshotHistory, setRenderIndex, setCleanComponentAtomTree} = snapshotSlice.actions; export default snapshotSlice.reducer; ================================================ FILE: src/app/state-management/slices/ThrottleSlice.tsx ================================================ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; const initialState: any = { throttleValue: '70', }; export const throttleSlice = createSlice({ name: 'throttle', initialState, reducers: { newThrottle: (state, action: PayloadAction) => { state.throttleValue = action.payload; }, resetThrottle: state => { state.throttleValue = '70'; }, }, }); export const {newThrottle, resetThrottle} = throttleSlice.actions; export default throttleSlice.reducer; ================================================ FILE: src/app/state-management/slices/ZoomSlice.tsx ================================================ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; import {RootState} from '../index'; interface ZoomState { zoomData: any; } const initialState: ZoomState = { // starting x, y positions on SVG canvas zoomData: { // x: 18, y: 527, k: 0.12 - updated x: 100, y: 527, k: 0.09 x: 220, y: 850, k: 0.09, }, }; export const zoomSlice = createSlice({ name: 'zoom', initialState, reducers: { updateZoomState: (state, action: PayloadAction) => { state.zoomData = action.payload; }, setDefaultZoom: state => { state.zoomData = initialState.zoomData; }, }, }); export const {updateZoomState, setDefaultZoom} = zoomSlice.actions; export const selectZoomState = (state: RootState) => state.zoom.zoomData; const zoomReducer = zoomSlice.reducer; export default zoomReducer; ================================================ FILE: src/app/utils/cleanComponentAtomTree.ts ================================================ import {componentAtomTree} from '../../types'; const generateCleanComponentAtomTree = ( inputObj: componentAtomTree, ): componentAtomTree => { const obj = {} as componentAtomTree; let counter = 0; const innerClean = (inputObj: any, outputObj: any, counter: number = 0) => { if ( (inputObj.tag === 0 || inputObj.tag === 2) && inputObj.name !== 'RecoilRoot' && inputObj.name !== 'Batcher' && inputObj.name !== 'RecoilizeDebugger' && inputObj.name !== 'CssBaseline' ) { // if the obj is empty, we do this if (Object.keys(obj).length === 0) { outputObj.children = []; outputObj.name = inputObj.name; outputObj.recoilNodes = inputObj.recoilNodes; outputObj.tag = inputObj.tag; outputObj = outputObj.children; } // create another conditional else { const deepCopy: componentAtomTree = JSON.parse( JSON.stringify(inputObj), ); deepCopy.children = []; outputObj.push(deepCopy); if (outputObj.length > 1) { outputObj = outputObj[outputObj.length - 1].children; } else { outputObj = outputObj[0].children; } } } // recursive call running through the whole component atom tree -- understand this better for (let i = 0; i < inputObj.children.length; i++) { innerClean(inputObj.children[i], outputObj, counter); } return outputObj; }; innerClean(inputObj, obj, counter); //ensure that the root element's actual duration is included in outObj if (inputObj.actualDuration) { obj.actualDuration = inputObj.actualDuration; } // returning the new object that we create return obj; }; export default generateCleanComponentAtomTree; ================================================ FILE: src/app/utils/makeRelationshipLinks.ts ================================================ type relationships = { nodes: any[]; links: any[]; }; type caches = { [key: string]: any; }; const makeRelationshipLinks = (obj: any) => { const relationships: relationships = {nodes: [], links: []}; if (!obj) return relationships; // relationships.nodes = makeNodes(obj); // to help with O(1) look up for nodeKeys & target => source combinations const nodeCache: caches = {}; const sourceTargetCache: caches = {}; // loops and push nodes into nodes array relationships.nodes = Object.keys(obj).map((nodeKey, index) => { nodeCache[nodeKey] = index; // make the node object and properties const objToReturn = { name: obj[nodeKey].type === 'RecoilState' ? 'Atom' : 'Selector', label: nodeKey, id: index, }; if (objToReturn.name === 'Atom') { if (obj[nodeKey].nodeDeps.length) { objToReturn.name = 'Selector'; } } return objToReturn; }); // loops node Data and push links into array Object.keys(obj).forEach((nodeKey, _index) => { // check all nodeToNode subscriptions (atoms to selectors) obj[nodeKey].nodeToNodeSubscriptions.forEach( (nodeToNodeSubscription: any) => { const targetSource = `${nodeCache[nodeKey]}${nodeCache[nodeToNodeSubscription]}`; if ( nodeCache[nodeToNodeSubscription] && !sourceTargetCache[targetSource] ) { sourceTargetCache[targetSource] = true; relationships.links.push({ source: nodeCache[nodeKey], target: nodeCache[nodeToNodeSubscription], }); } }, ); // check all nodeDeps subscriptions (selectors to atoms) obj[nodeKey].nodeDeps.forEach((nodeDeps: any) => { const targetSource = `${nodeCache[nodeDeps]}${nodeCache[nodeKey]}`; if (nodeCache[nodeDeps] && !sourceTargetCache[targetSource]) { sourceTargetCache[targetSource] = true; relationships.links.push({ source: nodeCache[nodeDeps], target: nodeCache[nodeKey], }); } }); }); return relationships; }; export default makeRelationshipLinks; ================================================ FILE: src/app/utils/makeTreeConversion.ts ================================================ type makeTreeObj = { [name: string]: any; }; const makeTree = (obj: any) => { // function that parses and refactors snapshotHistory into an object d3 can understand if (!obj) return; let result: any[] = []; let keys = Object.keys(obj); keys.forEach(key => { let newObj: makeTreeObj = {}; newObj['name'] = key; // obj[key] is a nested object so recurse if (typeof obj[key] === 'object' && !Array.isArray(obj[key]) && obj[key]) { newObj['children'] = makeTree(obj[key]); } else if (Array.isArray(obj[key])) { // obj[key] is an array newObj['children'] = []; obj[key].forEach((_el: any, i: number) => { newObj.children.push({ name: `${key}[${i}]`, value: obj[key][i], }); }); } else { // obj[key] is a primitive newObj.children = [ { name: JSON.stringify(obj[key]), }, ]; } result.push(newObj); }); return result; }; export default makeTree; ================================================ FILE: src/extension/background.ts ================================================ // TO CONSOLE LOG IN THIS FILE, we need to go to chrome://extensions/ and click inspect on the actual devTOOL! // Message Interface interface Msg { action: string; tabId?: string; payload?: object; test?: number; } interface Connections { [tabId: string]: any; } // once background-script start, start with cleared connections const connections: Connections = {}; // once background starts, start with cleared local storage // this only happens when we open chrome again, not on refresh chrome.storage.local.clear(function (): void { chrome.storage.local.get(null, function (result): void {}); }); // LISTEN for initial connection from dev tool // runs when devtool is connected chrome.runtime.onConnect.addListener(port => { const devToolsListener = (msg: Msg, port: object) => { console.log( 'in the onConnect of background script IN BG SCRIPT ', msg, port, ); const {tabId, action} = msg; switch (action) { case 'devToolInitialized': connections[tabId] = port; // read and send back to dev tool current local storage for corresponding tabId & port console.log('local chrome storage: ', chrome.storage.local); chrome.storage.local.get(null, function (result) { console.log('chrome.storage.local.get result: ', result); connections[tabId].postMessage({ action: 'recordSnapshot', payload: result[tabId], }); console.log('connections tabId: ', connections[tabId]); console.log('connections: ', connections); console.log('tabId: ', tabId); }); break; case 'snapshotTimeTravel': if (tabId) { // if msg tabId provided, send time travel snapshot history to content-script chrome.tabs.sendMessage(Number(tabId), msg); } break; case 'mouseover': if (tabId) { chrome.tabs.sendMessage(Number(tabId), msg); } break; case 'persistState': if (tabId) { // if msg tabId provided, send persistState command to content-script chrome.tabs.sendMessage(Number(tabId), msg); } break; case 'throttleEdit': if (tabId) { console.log('doing a throttle edit'); chrome.tabs.sendMessage(Number(tabId), msg); } // window.postMessage({action: 'throttleChange'}, throttler); break; default: break; } }; // BEGINS listening to messages from port port.onMessage.addListener(devToolsListener); // ENDS listening to messages from port port.onDisconnect.addListener(port => { // Removes listener port.onMessage.removeListener(devToolsListener); // Removes reference to devtool instance when the devtool is closed for (const prop in connections) { if (connections[prop] === port) { delete connections[prop]; break; } } }); }); // Listens to message from the Recoilize module chrome.runtime.onMessage.addListener((msg, sender) => { // Error handling if there isn't a proper tabId if (!sender.tab) return; console.log("background.ts chromeruntime msg: ", msg); // Grabs tab id from content script and converts it to a string const tabId = `${sender.tab.id}`; const {action} = msg; switch (action) { // Listens to new snapshots (state changes) from module, stores in local storage and sends to dev tool if port is opened case 'recordSnapshot': //console.log('chrome.runtime.onMessage with case: recordSnapshot'); // Next snapshot from the msg payload const snapshot = msg.payload; //console.log('snapshot: ', snapshot); // Get current snapshot history from local storage chrome.storage.local.get([tabId], function (result) { // Grab the current snapshot history from local storage const tabIdSnapshotHistory = result[tabId] ? [...result[tabId]] : []; // Grab the last (most recent) snapshot from the history const lastSnapshot = tabIdSnapshotHistory.length > 0 ? tabIdSnapshotHistory[tabIdSnapshotHistory.length - 1] : {}; // Merge the changed atoms from the new state with the old state // the old state should have the list of ALL atoms, not just the ones that changed, so we want to preserve a list of all atoms. tabIdSnapshotHistory.push(Object.assign({}, lastSnapshot, snapshot)); // Set local storage with updated snapshotHistory chrome.storage.local.set({[tabId]: tabIdSnapshotHistory}, function () { // ONLY if there is a port connection with the current tabId if (connections[tabId]) { // Send to dev tool connections[tabId].postMessage({ action: 'recordSnapshot', payload: tabIdSnapshotHistory, }); } }); }); break; // const devToolData = createDevToolDataObject( // initialFilteredSnapshot, // indexDiff, // atomsAndSelectorsMsg, // selectorsObject, // ); // If the module is loaded for first time or refreshed reset snapshot History and send initial payload case 'moduleInitialized': //console.log('module has been initialized IN BG SCRIPT', msg); const tabIdSnapshotHistory = [msg.payload]; // set tabId within local storage to initial snapshot sent from module chrome.storage.local.set({[tabId]: tabIdSnapshotHistory}, function () { // if tabId is has opened dev tool port, send snapshotHistory to dev tool. if (connections[tabId]) { connections[tabId].postMessage({ action: 'recordSnapshot', payload: tabIdSnapshotHistory, }); } }); break; case 'persistSnapshots': // getting the array of filtered snapshots that exists on locoal storage chrome.storage.local.get(tabId, function (result) { connections[tabId].postMessage({ action: 'recordSnapshot', payload: result[tabId], }); }); default: break; } }); // when the tab is closed reset local storage for that specific tabId chrome.tabs.onRemoved.addListener(tabId => { // we should only do this when tab closes not when port closes. chrome.storage.local.remove([`${tabId}`], function () { chrome.storage.local.get(null, function (result) {}); }); }); ================================================ FILE: src/extension/build/devtools.html ================================================ ⚛ Recoilize ================================================ FILE: src/extension/build/devtools.js ================================================ // The initial script that loads the extension into the DevTools panel. chrome.devtools.panels.create( 'Recoilize', // title of devtool panel '../assets/covalent-recoil-logo.jpg', // icon of devtool panel 'panel.html', // html of devtool panel ); ================================================ FILE: src/extension/build/diff.css ================================================ .jsondiffpatch-delta { font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace; font-size: 12px; margin: 0; padding: 0 0 0 12px; display: inline-block; } .jsondiffpatch-delta pre { font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace; font-size: 12px; margin: 0; padding: 0; display: inline-block; } ul.jsondiffpatch-delta { list-style-type: none; padding: 0 0 0 20px; margin: 0; } .jsondiffpatch-delta ul { list-style-type: none; padding: 0 0 0 20px; margin: 0; } .jsondiffpatch-added .jsondiffpatch-property-name, .jsondiffpatch-added .jsondiffpatch-value pre, .jsondiffpatch-modified .jsondiffpatch-right-value pre, .jsondiffpatch-textdiff-added { background: #5A6C46; } .jsondiffpatch-deleted .jsondiffpatch-property-name, .jsondiffpatch-deleted pre, .jsondiffpatch-modified .jsondiffpatch-left-value pre, .jsondiffpatch-textdiff-deleted { background: #7E5C69; text-decoration: line-through; } .jsondiffpatch-unchanged, .jsondiffpatch-movedestination { color: gray; } .jsondiffpatch-unchanged, .jsondiffpatch-movedestination > .jsondiffpatch-value { transition: all 0.5s; -webkit-transition: all 0.5s; overflow-y: hidden; } .jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged, .jsondiffpatch-unchanged-showing .jsondiffpatch-movedestination > .jsondiffpatch-value { max-height: 100px; } .jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged, .jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value { max-height: 0; } .jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value, .jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value { display: block; } .jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged, .jsondiffpatch-unchanged-visible .jsondiffpatch-movedestination > .jsondiffpatch-value { max-height: 100px; } .jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged, .jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value { max-height: 0; } .jsondiffpatch-unchanged-showing .jsondiffpatch-arrow, .jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow { display: none; } .jsondiffpatch-value { display: inline-block; } .jsondiffpatch-property-name { display: inline-block; padding-right: 5px; vertical-align: top; } .jsondiffpatch-property-name:after { content: ': '; } .jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after { content: ': ['; } .jsondiffpatch-child-node-type-array:after { content: '],'; } div.jsondiffpatch-child-node-type-array:before { content: '['; } div.jsondiffpatch-child-node-type-array:after { content: ']'; } .jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after { content: ': {'; } .jsondiffpatch-child-node-type-object:after { content: '},'; } div.jsondiffpatch-child-node-type-object:before { content: '{'; } div.jsondiffpatch-child-node-type-object:after { content: '}'; } .jsondiffpatch-value pre:after { content: ','; } li:last-child > .jsondiffpatch-value pre:after, .jsondiffpatch-modified > .jsondiffpatch-left-value pre:after { content: ''; } .jsondiffpatch-modified .jsondiffpatch-value { display: inline-block; } .jsondiffpatch-modified .jsondiffpatch-right-value { margin-left: 5px; } .jsondiffpatch-moved .jsondiffpatch-value { display: none; } .jsondiffpatch-moved .jsondiffpatch-moved-destination { display: inline-block; background: #ffffbb; color: #888; } .jsondiffpatch-moved .jsondiffpatch-moved-destination:before { content: ' => '; } ul.jsondiffpatch-textdiff { padding: 0; } .jsondiffpatch-textdiff-location { color: #bbb; display: inline-block; min-width: 60px; } .jsondiffpatch-textdiff-line { display: inline-block; } .jsondiffpatch-textdiff-line-number:after { content: ','; } .jsondiffpatch-error { background: red; color: white; font-weight: bold; } ================================================ FILE: src/extension/build/manifest.json ================================================ { "name": "Recoilize_Testing", "version": "3.0.0", "devtools_page": "devtools.html", "description": "A Chrome extension that helps debug Recoil applications by memorizing the state of components with every render.", "manifest_version": 2, "content_security_policy": "script-src 'self' 'unsafe-eval' ; object-src 'self'", "icons": { "16": "assets/Recoilize-v2.png", "48": "assets/Recoilize-v2.png", "128": "assets/Recoilize-v2.png" }, "permissions": ["storage"], "background": { "scripts": ["bundles/background.bundle.js"], "persistent": false }, "content_scripts": [ { "matches": [""], "js": ["bundles/content.bundle.js"] } ] } ================================================ FILE: src/extension/build/panel.html ================================================
    ================================================ FILE: src/extension/build/stylesheet.css ================================================ /* GLOBAL CSS */ html, body { color: #989898; margin: 0; height: 100%; background-color: #212121; } ::-webkit-scrollbar { background-color: transparent; } ::-webkit-scrollbar-track { background-color: rgba(255, 255, 255, 0.1); } ::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.15); border: 2px solid rgba(255, 255, 255, 0.1); border-radius: 7px; } ::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.25); border: 2px solid rgba(255, 255, 255, 0.2); } /* remove outlines on input boxes and buttons in all pages of the app */ input, button { outline: none; } /* basic css for the button and button:hover for uniform look */ button { border: 1px solid #989898; border-radius: 5px; color: #e6e6e6; background: none; } button:hover { color: white; border: 1px solid white; background-color: #212121; text-emphasis: bold; } /* APP -> MAIN CONTAINER */ .App, #root { width: 100%; height: 100%; } .notFoundContainer { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100%; text-align: center; font-size: 1.2rem; } .notFoundContainer p { margin: 10px 20px; } .notFoundContainer a { color: #3578e5; } .logo { width: 100px; height: auto; } /* MAIN CONTAINER -> * 0) SNAPSHOTS CONTAINER, * 1) VISUAL CONTAINER, */ .MainContainer { height: 100%; width: 100%; display: grid; grid-template-columns: min-content 2fr; grid-template-rows: 8fr 1fr; grid-template-areas: 'actions states' 'travel travel' 'buttons buttons'; overflow: hidden; } /* Time Travel Container */ .travel-container { width: 100%; grid-area: travel; background: linear-gradient( 90deg, rgba(41, 41, 41, 1) 0%, rgba(51, 51, 51, 1) 50%, rgba(41, 41, 41, 1) 100% ); position: absolute; bottom: 0px; display: flex; flex-direction: column; } .main-slider { /* if changed, other css attributes will also be affected: position: absolute; bottom: 0px; */ display:inline-flex; /* side effect */ /* creates a black box above slider (not covering snapshot list) */ margin-top: 10px; } #slider-start-button { width: 100px; height: 25px; margin: 0px 5px 5px 10px; } .backfor-button { width: 30px; height: 25px; margin: 0px 10px 10px 5px; /* margin: 0 0 1% 1%; */ } .rc-slider { position: relative; width: calc(100% - 170px); margin: 10px; border-radius: 6px; -ms-touch-action: none; touch-action: none; box-sizing: border-box; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .rc-slider * { box-sizing: border-box; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .rc-slider-rail { position: absolute; width: 100%; background-color: #ebf2fa; height: 4px; border-radius: 6px; } .rc-slider-track { position: absolute; left: 0; height: 4px; border-radius: 6px; background-color: #ff5470; } .rc-slider-handle { position: absolute; margin-left: 5px; margin-top: -10px; padding-left: 10px; width: 20px; height: 20px; cursor: pointer; cursor: -webkit-grab; cursor: grab; border-radius: 50%; background-color: #bd4f6c; background-image: linear-gradient(326deg, #bd4f6c 0%, #d7816a 74%); -ms-touch-action: pan-x; touch-action: pan-x; } /* Buttons Container */ .buttons_container { grid-area: buttons; display: flex; } #docs_button { width: 100px; height: 25px; margin: 0px 5px 15px 10px; } /* SNAPSHOT CONTAINER -> SNAPSHOTLIST COMPONENT */ .SnapshotsContainer { /* adjustment for snapshot list & scrollbar */ height: calc(100% - 40px); display: flex; flex-direction: column; justify-content: stretch; flex-shrink: 1; min-width: 170px; background-color: #2d2d2d; text-align: center; color: #e6e6e6; grid-area: actions; overflow: auto; } .save-series-button { width: 50%; margin-left: 45px; margin-bottom: 5px; } .removeSnapshot { background-color: transparent; border: none; color: white; /* outline: none; */ font-size: 0.7em; } .removeSnapshot:hover { opacity: 50%; } .removeSnapshot:active { opacity: 50%; background-color: black; } .clear-buttons { display: flex; padding-bottom: 1em; border-bottom: 1px solid #646464; } #clear-snapshots-title { padding: 1em; font-size: 12px; font-weight: bold; } #prevClr { margin-left: 0.9em; width: 5em; cursor: pointer; } #fwrdClr { margin-right: 0.9em; width: 5em; margin-left: auto; cursor: pointer; } /* SNAPSHOTLIST COMPONENT */ .SnapshotsList { height: calc(100% - 40px); overflow: auto; border-style: none; } .individualSnapshot { list-style-type: none; display: flex; flex-grow: 1; padding: 10px; padding-left: 30px; padding-right: 20px; border: none; justify-content: space-between; } .individualSnapshot:hover { background-color: #212121; } .timeTravelButton { color: #989898; } .timeTravelButton:focus { color: #e6e6e6; background-color: #212121; } /* VISUAL CONTAINER -> * 0) NAVBAR, * 1) DIFF, * 2) TREE, * 3) VISUALIZER, * 4) NETWORK, * 5) ATOMCOMPONENTVISUAL CONTAINER, * 6) SETTINGS, */ .VisualContainer { /* height && margin-top */ /* affects how scroll bar looks within State Diff && State Tree tabs */ height: calc(100% - 33px - 20px); margin-top: 33px; margin-bottom: 20px; width: calc(100% - 3px); display: flex; flex-grow: 8; flex-direction: column; border-left: 2px solid #484848; grid-area: states; color: #e6e6e6; /* overflow: auto; is important for visual consistency */ /* affects initial loading of NavBar && State Diff */ /* affects scrolling of State Diff && State Tree */ overflow: auto; /* overflow-y: auto messes up the height styling */ } /* NAVBAR COMPONENT */ .NavBar { width: calc(100% - 174px); display: flex; flex-direction: row; text-align: center; border-right: 2px solid #484848; border-bottom: 2px solid #484848; background-color: #2d2d2d; position: absolute; top: 0px; z-index: 1; } .navBarButtons { /* flex-grow: 1; */ color: #989898; background-color: #2d2d2d; border: none; padding: 8px; padding-left: 10px; padding-right: 10px; } .navBarButtons:hover { color: #e6e6e6; border: none; } /* DIFF COMPONENT */ .Diff { /* 33px is height of navbar */ height: calc(100% - 33px); /* height affects how scrollbar works/looks in State Diff tab */ border-style: none; display: flex; flex-direction: column; padding: 10px; padding-bottom: 20px; overflow: auto; } .toggleDiv { /* display: flex; justify-content: flex-end; */ overflow: auto; } #raw { cursor: pointer; } .rawToggle { background: none; border: none !important; } /* TREE COMPONENT */ .Tree { /* adjustment for height of navbar (33px) */ height: calc(100% - 33px - 20px); width: 90%; /* height & width affect how scrollbar works/looks in State Tree tab */ display: flex; flex-direction: column; padding: 10px; /* overflow: auto; */ /* not the one needing overflow scrolling */ } .json-tree { /* adjustment for height of navbar (33px) */ height: calc(100% - 33px - 25px); margin-bottom: 20px; padding-bottom: 20px; width: 100%; /* padding-bottom: 20px; */ background-color: none; list-style: none; /* not the one needing overflow scrolling */ } /* VISUALIZER COMPONENT */ /* Flame Graph */ #metricsWrapper { height: calc(100% - 33px - 50px); /* width: 98% fixes moving-bars issue */ width: 98%; overflow: hidden; } .RankedGraph, #canvas { /* #canvas --> Component Graph */ flex-grow: 1; width: 100%; overflow: hidden; } #stateGraphContainer { /* 33px is height of navbar */ height: calc(100% - 33px); width: 100%; overflow: auto; } /* svg { height: calc(100% - 33px - 50px); } */ .svg-container { display: inline-block; position: relative; width: 100%; vertical-align: top; overflow: hidden; } .svg-content-responsive { display: flex; align-items: center; justify-content: center; position: absolute; top: 10px; left: 0; } .graphContainer { padding: 10px; } .graphButton { color: #989898; } .graphButton:hover { color: #e6e6e6; border: 1px solid white; } .graphButton:focus { color: #e6e6e6; background-color: #212121; } /* NETWORK COMPONENT */ .networkContainer { height: 100%; width: 100%; } .Network { position: relative; height: 100%; width: 100%; } #networkCanvas { position: relative; height: 100%; width: 100%; } #networkSearch { width: 135px; grid-row: 1; } .LegendContainer { display: flex; flex-direction: column; align-items: flex-start; justify-items: flex-start; } .AtomNetworkLegend { position: fixed; top: 58px; left: 179px; padding-left: 5px; padding-top: 5px; display: flex; flex-direction: column; align-items: flex-start; } .graph-slider { position: relative; top: 0; left: 0; } #spacingSliders { position: fixed; top: 72px; left: 375px; width: 150px; height: 70px; display: flex; flex-direction: column; justify-content: space-between; color: #e6e6e6; } .sliderContainer { display: flex; justify-content: space-between; } .sliderLabel { width: 10px; padding: 5px; } .siblingSlider, .parentChildSlider { -webkit-appearance: none; border-radius: 5px; width: 125px; background: none; } .siblingSlider::-webkit-slider-runnable-track, .parentChildSlider::-webkit-slider-runnable-track { background-color: #e6e6e6; border-radius: 5px; height: 4px; } .siblingSlider::-webkit-slider-thumb, .parentChildSlider::-webkit-slider-thumb { -webkit-appearance: none; background-color: #bd4f6c; background-image: linear-gradient(326deg, #bd4f6c 0%, #d7816a 74%); border: none; height: 20px; width: 20px; border-radius: 50%; cursor: grab; margin-top: -8px; padding-left: 10px; } .graphBtnContainer { position: fixed; top: 150px; left: 375px; width: 150px; height: 65px; display: flex; justify-content: space-between; align-items: center; padding: 5px 15px; } .graphBtn { display: flex; } #orientationBtn { width: 65px; height: 50px; background: transparent; border: none !important; margin-right: 5px; cursor: pointer; padding-right: 5px; color: #989898; } .AtomDiv { display: grid; grid-row: 2; grid-template-columns: 1fr 1fr; align-items: center; gap: 1em; } .SelectorDiv { display: grid; grid-row: 3; grid-template-columns: 1fr 1fr; align-items: center; gap: 1em; } .AtomDiv:hover, .SelectorDiv:hover, .AtomP:hover, .SelectorP:hover { opacity: 90% !important; cursor: pointer; z-index: 1; } .AtomDiv:active, .SelectorDiv:active, .AtomP:active, .SelectorP:active { opacity: 50% !important; } .AtomLegend { display: inline-block; background-color: #9580ff; width: 20px; height: 20px; border: 1px solid #9580ff; border-radius: 20px; margin: 0px; flex-grow: 1; } .SelectorLegend { display: inline-block; background-color: #ff80bf; width: 20px; height: 20px; border: 1px solid #ff80bf; border-radius: 20px; margin: 0px; flex-grow: 1; } .AtomDropdown, .SelectorDropdown { display: grid; grid-row: 4; border: 0.1em solid rgb(43, 34, 34); } .atom-class, .selector-class, .AtomListItem, .SelectorListItem { cursor: pointer; } .SelectorDropdown p:active, .AtomDropdown p:active { opacity: 80%; } /* ATOMCOMPONENTVISUAL CONTAINER -> * 0) ATOMCOMPONENTVISUAL COMPONENT * 1) ATOMSELECTORLEGEND COMPONENT */ .Component { /* position: relative; */ height: 100%; width: 100%; } /* ATOMCOMPONENTVISUAL COMPONENT */ .AtomComponentVisual, #canvas { position: relative; height: 100%; width: 100%; } .hoverInfo { height: min-content; width: max-content; border: 0; border-radius: 5px; position: fixed; background-color: #484848; font-size: 100%; padding: 0 1%; z-index: 1; } #componentGraph { transform-origin: 15% -90%; } #fixedButton { width: 65px; height: 50px; background: transparent; border: none !important; margin-right: 5px; cursor: pointer; color: #989898; } .RecoilSearch { position: fixed; top: 58px; left: 179px; display: grid; grid-template-columns: 30px 30px; grid-template-rows: 30px 30px 30px 1fr; align-items: center; padding-left: 5px; padding-top: 5px; } .AtomNetworkLegend { position: fixed; top: 58px; left: 179px; display: grid; grid-template-columns: 30px 30px; grid-template-rows: 30px 30px 30px 1fr; align-items: center; padding-left: 5px; padding-top: 5px; } .AtomNetworkLegendWithSearch { position: fixed; top: 100px; left: 179px; display: grid; grid-template-columns: 30px 30px; grid-template-rows: 30px 30px 30px 1fr; align-items: center; padding-left: 5px; padding-top: 5px; } .AtomLegend { display: inline-block; background-color: #9580ff; width: 20px; height: 20px; border: 1px solid #9580ff; border-radius: 20px; margin: 0px; } .SelectorLegend { display: inline-block; background-color: #ff80bf; width: 20px; height: 20px; border: 1px solid #ff80bf; border-radius: 20px; margin: 0px; } .bothLegend { display: inline-block; background-color: springgreen; width: 20px; height: 20px; border: 1px solid springgreen; border-radius: 20px; margin: 0px; } #atomDrop, #selectorDrop { display: grid; grid-row: 5; } .selectorSelected:hover { color: #ff80bf; background-color: rgb(240, 240, 162); border-color: white; width: 120px; opacity: 100%; } .atomSelected:hover { color: #9580ff; background-color: rgb(240, 240, 162); border-color: white; width: 120px; opacity: 100%; } .atomSelected { color: #9580ff; background-color: rgb(240, 240, 162); border-color: white; width: 120px; opacity: 100%; } .atomNotSelected { color: #9580ff; border-color: white; width: 120px; opacity: 30%; } .atomDropDown { color: #9580ff; border-color: white; width: 120px; } .atomLegendDefault { color: #9580ff; border-color: white; width: 120px; } .selectorSelected { color: #ff80bf; background-color: rgb(240, 240, 162); border-color: white; width: 120px; opacity: 100%; } .selectorNotSelected { color: #ff80bf; border-color: white; width: 120px; opacity: 30%; } .selectorDropDown { color: #ff80bf; border-color: white; width: 120px; } .selectorLegendDefault { color: #ff80bf; border-color: white; width: 120px; } .bothLegendDefault { color: springgreen; border-color: white; width: 120px; } .suspenseLegend { display: inline-block; background: transparent; width: 20px; height: 20px; border: 3px solid red; border-radius: 20px; margin: 0px; } .dropDownButtonDiv { margin: 5px; } .LegendButtons { display: flex; flex-direction: column; margin: 5px; margin-top: 60px; } /* ATOMSELECTORLEGEND COMPONENT */ .AtomSelectorLegend { background-color: rgba(45, 45, 45, 0.65); position: fixed; right: 0; border: 2px solid #484848; top: 2.5rem; width: 250px; max-height: 400px; overflow-y: auto; text-align: center; margin-right: 5px; } .atomLi, .selectorLi { list-style-type: none; padding: 10px; padding-left: 20px; padding-right: 20px; border: none; word-wrap: break-word; font-size: 14px; z-index: 1; } .atomLi:hover, .selectorLi:hover { background-color: #212121; z-index: 1; } .minimizeButton { margin: 5px; border-radius: 7px; width: 80px; height: 22px; cursor: pointer; font-weight: bold; } .minimize { display: none; } /* SETTINGS CONTAINER -> * 0) ATOM SETTINGS, * 1) STATE SETTINGS, * 2) THROTTLE SETTINGS, */ .Settings { height: calc(100% - 33); overflow: auto; border-style: none; display: flex; flex-direction: column; padding: 10px; padding-bottom: 20px; } /* ! Extra for settings */ /* Dropdown Button */ .dropbtn { background-color: #4caf50; color: white; padding: 16px; font-size: 16px; border: none; cursor: pointer; } /* Dropdown button on hover & focus */ .dropbtn:hover, .dropbtn:focus { background-color: #3e8e41; } /* The container
    - needed to position the dropdown content */ .dropdown { position: relative; display: inline-block; color: white; } /* Dropdown Content (Hidden by Default) */ .dropdown-content { display: none; position: absolute; background-color: #f9f9f9; min-width: 160px; box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); } /* Links inside the dropdown */ .dropdown-content a { color: black; padding: 12px 16px; text-decoration: none; display: block; } /* Change color of dropdown links on hover */ .dropdown-content a:hover { background-color: #f1f1f1; } /* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */ .show { display: block; } /* STATE SETTINGS COMPONENT */ .persistContainer { display: table; } .switch { display: inline-block; height: 24px; position: relative; width: 45px; } .switch input { display: none; } .persistText { display: table-cell; vertical-align: middle; font-size: 14px; padding-left: 10px; } .slider { background-color: #ccc; cursor: pointer; position: absolute; top: 0; bottom: 0; left: 0; right: 0; transition: 0.4s; } .slider:before { background-color: #fff; bottom: 4px; content: ''; height: 15px; left: 4px; position: absolute; transition: 0.4s; width: 15px; } input:checked + .slider { background-color: #0096fb; } input:checked + .slider:before { transform: translateX(22px); } .slider.round { border-radius: 34px; } .slider.round:before { border-radius: 50%; } /* Doesn't really do anything because other elements stack on top of it, * but scrollbar is broken without it */ /* Code added for verticle icicle graph */ .path { will-change: d; } .path:hover { cursor: pointer; } ================================================ FILE: src/extension/contentScript.ts ================================================ // once chrome tab connects with our content-script window.postMessage({action: 'contentScriptStarted'}, '*'); //console.log('content script sending action to the window IN CONTENT SCRIPT'); // Listen to messages from Recoilize module within dev webpage window.addEventListener('message', msg => { console.log('contentScript message: ', msg.data); chrome.runtime.sendMessage(msg.data); }); // listening for messages from the background script chrome.runtime.onMessage.addListener(msg => { // send the message to npm package const {action} = msg; switch (action) { case 'snapshotTimeTravel': window.postMessage(msg, '*'); break; case 'persistState': window.postMessage(msg, '*'); break; case 'throttleEdit': window.postMessage(msg, '*'); break; } }); ================================================ FILE: src/types/index.d.ts ================================================ // snapshot taken by recoilize module export type stateSnapshot = { filteredSnapshot: filteredSnapshot; componentAtomTree: componentAtomTree; indexDiff?: number; }; // used for the filter state hook export type stateSnapshotDiff = { filteredSnapshot?: filteredSnapshotDiff; componentAtomTree?: componentAtomTreeDiff; indexDiff?: number; }; export type filteredSnapshot = { // key of atom name with the value of an atom [atomName: string]: node; }; export type filteredSnapshotDiff = { [atomName: string]: nodeDiff; }; // object of either atom or selector export type node = { // recoil defined string that determines wether an atom or selector distinguished by 'RecoilState' or 'RecoilValueReadOnly' type: string; // user defined node state contents: any; // current node is dependent on this array nodeDeps: string[]; // current node is a dependency for the array of nodes nodeToNodeSubscription: string[]; }; export type nodeDiff = { string?: string | string[]; contents?: any; nodeDeps?: string[] | string[][]; nodeToNodeSubscription?: string[] | string[][]; }; export type componentAtomTree = { children: object[]; name: string; tag: number; recoilNodes: string[]; actualDuration: number; treeBaseDuration: number; wasSuspended: boolean; }; export type componentAtomTreeDiff = { children: object[] | object[][]; name: string | string[]; tag: number | number[]; recoilNodes: string[] | string[][]; actualDuration: number | number[]; treeBaseDuration: number | number[]; wasSuspended: boolean | boolean[]; }; export type dataDuration = { [name: string]: any; }; export type dataDurationArr = dataDuration[]; //! not possible to be typed better // atom is an object consisting of // atom state names and their values. // Since state can be anything (num, bool, str, etc.) // it's impossible to say what properties // atom can hold other than some generic key name and // a type of any export type atom = { [name: string]: any; }; //! not possible to be typed better // selector is an object consisting of // selector state names and their values. // Since state can be anything (num, bool, str, etc.) // it's impossible to say what properties // selector can hold other than some generic key name and // a type of any export type selector = { [name: string]: any; }; export type selectedTypes = { [name: string]: string; }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "outDir": "./build", "module": "commonjs", "pretty": true, "noImplicitAny": false, "removeComments": true, "preserveConstEnums": true, "sourceMap": true, "allowJs": true, "jsx": "react", "target": "esnext", "esModuleInterop": true }, "include": ["./src/app/**/*", "./index.d.ts"], "exclude": ["node_modules", ".vscode", "__tests__"] } ================================================ FILE: webpack.config.js ================================================ const path = require('path'); const ChromeExtensionReloader = require('webpack-chrome-extension-reloader'); const config = { entry: { app: './src/app/index.tsx', background: './src/extension/background.ts', content: './src/extension/contentScript.ts', }, output: { path: path.resolve(__dirname, 'src/extension/build/bundles'), filename: '[name].bundle.js', }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, { test: /\.jsx?/, exclude: /(node_modules)/, resolve: { extensions: ['.js', '.jsx'], }, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'], }, }, }, { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }, resolve: { extensions: ['.tsx', '.ts', '.js', '.jsx'], }, plugins: [], }; module.exports = (env, argv) => { if (argv.mode === 'development') { config.plugins.push( new ChromeExtensionReloader({ entries: { contentScript: ['app', 'content'], background: ['background'], }, }), ); } return config; };