├── .babelrc
├── .eslintrc
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CREDITS.md
├── LICENSE
├── README.md
├── examples
├── demo-app.js
├── demo.html
├── extension.png
├── graph.png
├── simple-graph.js
├── your-app.html
└── your-app.js
├── extension
├── .babelrc
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── Screen Shot 2017-11-15 at 3.34.11 AM.png
├── TODO.md
├── app
│ ├── actions
│ │ └── graph.js
│ ├── components
│ │ ├── Dock.js
│ │ ├── Footer.css
│ │ ├── Footer.js
│ │ ├── Header.js
│ │ ├── MainSection.css
│ │ ├── MainSection.js
│ │ ├── SelectorGraph.js
│ │ ├── SelectorInspector.js
│ │ ├── SelectorSearch.css
│ │ ├── SelectorSearch.js
│ │ ├── SelectorState.css
│ │ ├── SelectorState.js
│ │ ├── StateTree.js
│ │ ├── TodoItem.css
│ │ ├── TodoItem.js
│ │ ├── TodoTextInput.css
│ │ └── TodoTextInput.js
│ ├── constants
│ │ ├── ActionTypes.js
│ │ └── TodoFilters.js
│ ├── containers
│ │ ├── App.js
│ │ └── Root.js
│ ├── reducers
│ │ ├── graph.js
│ │ └── index.js
│ ├── store
│ │ ├── configureStore.dev.js
│ │ ├── configureStore.js
│ │ └── configureStore.prod.js
│ └── utils
│ │ ├── apiMiddleware.js
│ │ ├── rpc.js
│ │ └── storage.js
├── appveyor.yml
├── chrome
│ ├── assets
│ │ └── img
│ │ │ ├── icon-128-disabled.png
│ │ │ ├── icon-128.png
│ │ │ ├── icon-16-disabled.png
│ │ │ ├── icon-16.png
│ │ │ ├── icon-32.png
│ │ │ ├── icon-48-disabled.png
│ │ │ └── icon-48.png
│ ├── extension
│ │ ├── background.js
│ │ ├── devtools.js
│ │ ├── page-api.js
│ │ ├── panel.js
│ │ ├── reselect-tools-app.css
│ │ └── reselect-tools-app.js
│ ├── manifest.dev.json
│ ├── manifest.prod.json
│ └── views
│ │ ├── background.pug
│ │ ├── devtools.pug
│ │ └── panel.pug
├── index.html
├── package.json
├── scripts
│ ├── .eslintrc
│ ├── build.js
│ ├── compress.js
│ ├── dev.js
│ └── tasks.js
├── test
│ ├── .eslintrc
│ ├── app
│ │ ├── actions
│ │ │ └── todos.spec.js
│ │ ├── components
│ │ │ ├── Footer.spec.js
│ │ │ ├── Header.spec.js
│ │ │ ├── MainSection.spec.js
│ │ │ ├── TodoItem.spec.js
│ │ │ └── TodoTextInput.spec.js
│ │ └── reducers
│ │ │ └── todos.spec.js
│ ├── e2e
│ │ ├── injectpage.js
│ │ └── todoapp.js
│ ├── func.js
│ ├── mocha.opts
│ └── setup-app.js
├── webpack
│ ├── .eslintrc
│ ├── customPublicPath.js
│ ├── dev.config.js
│ ├── postcss.config.js
│ ├── prod.config.js
│ ├── replace
│ │ ├── JsonpMainTemplate.runtime.js
│ │ └── process-update.js
│ └── test.config.js
└── yarn.lock
├── lib
└── index.js
├── package.json
├── src
└── index.js
└── test
└── test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"],
3 | "plugins": [
4 | ],
5 | "env": {
6 | "commonjs": {
7 | "plugins": [
8 | ["transform-es2015-modules-commonjs", { "loose": true }]
9 | ]
10 | },
11 | "umd": {
12 | "plugins": [
13 | ["transform-es2015-modules-umd",
14 | {
15 | "loose": true,
16 | "globals": {
17 | 'reselect': 'Reselect'
18 | }
19 | }]
20 | ],
21 | "moduleId": "ReselectTools"
22 | },
23 | "test": {
24 | "plugins": [
25 | ["transform-es2015-modules-commonjs", { "loose": true }],
26 | ]
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "mocha": true,
6 | "es6": true,
7 | },
8 | "extends": "eslint:recommended",
9 | "parserOptions": {
10 | "sourceType": "module"
11 | },
12 | "rules": {
13 | "array-bracket-spacing": [2, "always"],
14 | "eol-last": 2,
15 | "indent": [2, 2, {
16 | "SwitchCase": 1
17 | }],
18 | "no-multiple-empty-lines": 2,
19 | "object-curly-spacing": [2, "always"],
20 | "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
21 | "semi": [2, "never"],
22 | "strict": 0,
23 | "space-before-blocks": [2, "always"],
24 | "space-before-function-paren": [2, {"anonymous":"always","named":"never"}]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .nyc_output
3 | coverage
4 | dist
5 | es
6 | .vscode
7 | .idea
8 | typescript_test/should_compile/index.js
9 | typescript_test/should_not_compile/index.js
10 | typescript_test/common.js
11 | flow_test/should_fail/flow-typed/index.js.flow
12 | flow_test/should_pass/flow-typed/index.js.flow
13 | .DS_STORE
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4"
4 | - "5"
5 | - "6"
6 | - "7"
7 | script:
8 | - npm run lint
9 | - npm test
10 | - npm run test:cov
11 | - npm run compile
12 |
13 | after_success:
14 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js
15 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | All notable changes to this project will be documented in this file.
4 | This project adheres to [Semantic Versioning](http://semver.org/).
5 |
6 | ## [v0.0.7] - 2018/11/04
7 |
8 | ### New Features
9 |
10 | Removed need for createSelectorWithDependencies! (#6)
11 | Exception handling in the selectorGraph() (#10)
12 |
--------------------------------------------------------------------------------
/CREDITS.md:
--------------------------------------------------------------------------------
1 | # CREDITS
2 |
3 | * Thanks to all commenters on the proposal [here](https://github.com/reactjs/reselect/issues/279)
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2016 Reselect Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Reselect Tools
2 | [![Travis][build-badge]][build]
3 | [![npm package][npm-badge]][npm]
4 | [![Coveralls][coveralls-badge]][coveralls]
5 |
6 | Tools for working with the [reselect](https://github.com/reactjs/reselect) library:
7 | *Check selector dependencies, inputs, outputs, and recomputations at any time without refreshing!*
8 |
9 | 
10 |
11 |
12 |
13 | ```js
14 | export const data$ = (state) => state.data;
15 | export const ui$ = (state) => state.ui;
16 | export const users$ = createSelector(data$, (data) => data.users);
17 | export const currentUser$ = createSelector(ui$, users$, (ui, users) => users[ui.currentUser]);
18 | ...
19 |
20 | // in configureStore.js
21 | import * as selectors from './selectors.js'
22 | import * as ReselectTools from 'reselect-tools'
23 |
24 | ReselectTools.getStateWith(() => store.getState()) // allows you to get selector inputs and outputs
25 | ReselectTools.registerSelectors(selectors) // register string names for selectors
26 | ...
27 | ReselectTools.checkSelector('currentUser$')
28 | => {
29 | inputs: [{currentUser: 1}, users: {1: {name: 'sam'}}]
30 | outputs: {name: 'sam'},
31 | dependencies: [ui$, users$],
32 | recomputations: 1,
33 | isNamed: false,
34 | selectorName: 'currentUser$'
35 | }
36 | selectorGraph()
37 | => {
38 | nodes: {
39 | "data$": {
40 | name: "data$",
41 | recomputations: "N/A"
42 | },
43 | "ui$": {
44 | name: "ui$",
45 | recomputations: "N/A"
46 | },
47 | "users$": {
48 | name: "user$",
49 | recomputations: 1
50 | },
51 | "currentUser$": {
52 | name: "currentUser$",
53 | recomputations: 1
54 | },
55 | },
56 | edges: [
57 | { from: users$, to: data$ },
58 | { from: users$, to: data$ },
59 | { from: currentUser$, to: users$ },
60 | { from: currentUser$, to: ui$ },
61 | ]
62 | }
63 | ```
64 |
65 | ## Table of Contents
66 |
67 | - [Motivation](#motivation)
68 | - [Getting Started](#getting-started)
69 | - [Example](#example)
70 | - [API](#api)
71 | - [`getStateWith`](#getstatewithfunc)
72 | - [`checkSelector`](#checkselectorselector)
73 | - [`selectorGraph`](#selectorgraphselectorkey--defaultselectorkey)
74 | - [`registerSelectors`](#registerselectorskeyselectorobj)
75 | - [License](#license)
76 |
77 | ## Motivation
78 |
79 | It's handy to visualize the application state tree with the [Redux Devtools](https://github.com/zalmoxisus/redux-devtools-extension). But I was using selectors a lot, and there was no easy way to visualize the *computed state tree*. So, I created this library to output graphs like this one:
80 |
81 | 
82 |
83 | This library was intended to be used with the [chrome extension](./extension). However, it can be still be [useful without the chrome extension installed](#without-the-extension). The chrome extension will be useless without this library.
84 |
85 | See the original reselect issue [here](https://github.com/reactjs/reselect/issues/279).
86 |
87 | ## Getting Started
88 |
89 | Firstly, I apologize in advance that this section is required. It would be great to match the experience of installing redux devtools or react's. Hopefully the tools will be more tightly integrated with reselect at some point and these steps won't be necessary.
90 |
91 | 1. Install the Package
92 |
93 | npm install -s reselect-tools
94 |
95 | 2. Grab the [Chrome Extension](https://chrome.google.com/webstore/detail/reselect-devtools/cjmaipngmabglflfeepmdiffcijhjlbb)
96 |
97 | 3. Building the Graph:
98 |
99 | In order to start building out the selector graph, we need to tell the devtools about the selectors.
100 | ```
101 | import { registerSelectors } from 'reselect-tools'
102 | registerSelectors({ mySelector$ })
103 | ```
104 | If you're keeping all your selectors in the same place, this is dead simple:
105 | ```
106 | import * as selectors from './selectors.js'
107 | registerSelectors(selectors)
108 | ```
109 |
110 | That's it! At this point you should be able to open the devtools and view the selector graph.
111 |
112 | The tools will automatically discover and name dependencies of the selectors. If you want to override the name of a selector, you can do so:
113 | ```
114 | const foo$ = createSelector(bar$, (foo) => foo + 1);
115 | foo$.selectorName = 'bar$' // selector will show up as 'bar'
116 | ```
117 |
118 | 4. Checking Selector Inputs and Outputs
119 |
120 | Imagine that your tests are passing, but you think some selector in your app might be receiving bad input from a depended-upon selector. Where in the chain is the problem? In order to allow ```checkSelector``` and by extension, the extension, to get this information, we need to give Reselect Tools some way of feeding state to a selector.
121 | ```
122 | import store from './configureStore'
123 | ReselectTools.getStateWith(() => store.getState())
124 | ```
125 |
126 | ## Example
127 |
128 | The example is running [here](http://skortchmark.com/reselect-tools/examples/demo.html). Grab the extension and take a look!
129 |
130 | npm run example
131 |
132 | ## API
133 |
134 | ### getStateWith(func)
135 |
136 | `getStateWith` accepts a function which returns the current state. This state is then passed into ```checkSelector```. In most cases, this will be ```store.getState()```
137 |
138 | ### checkSelector(selector)
139 |
140 | Outputs information about the selector at the given time.
141 |
142 | By default, outputs only the recomputations of the selector.
143 | If you use ```getStateWith```, it will output the selector's input and output values.
144 | If you use ```registerSelectors```, you can pass it the string name of a selector.
145 |
146 |
147 | ```js
148 | const two$ = () => 2;
149 | const four$ = () => 4
150 | const mySelector$ = createSelector(two$, four$, (two, four) => two + four)
151 | registerSelectors({ mySelector$ })
152 | getStateWith(() => null)
153 |
154 | checkSelector('mySelector$') // {
155 | inputs: [2, 4],
156 | output: 6,
157 | dependencies: [two$, four$],
158 | recomputations: 1,
159 | }
160 | ```
161 |
162 |
163 | ### selectorGraph(selectorKey = defaultSelectorKey)
164 |
165 | ```selectorGraph``` outputs a POJO with nodes and edges. A node is a selector in the tree, and an edge goes from a selector to the selectors it depends on.
166 |
167 | ```js
168 | selectorGraph()
169 | // {
170 | // nodes: {
171 | // "data$": {
172 | // name: "data$",
173 | // recomputations: "N/A"
174 | // },
175 | // "ui$": {
176 | // name: "ui$",
177 | // recomputations: "N/A"
178 | // },
179 | // "users$": {
180 | // name: "user$",
181 | // recomputations: 1
182 | // },
183 | // "currentUser$": {
184 | // name: "currentUser$",
185 | // recomputations: 1
186 | // },
187 | // },
188 | // edges: [
189 | // { from: users$, to: data$ },
190 | // { from: users$, to: data$ },
191 | // { from: currentUser$, to: users$ },
192 | // { from: currentUser$, to: ui$ },
193 | // ]
194 | // }
195 | ```
196 |
197 | #### Using custom selectorKeys
198 |
199 | Nodes in the graph are keyed by string names. The name is determined by the ```selectorKey``` function. This function takes a selector and outputs a string which must be unique and consistent for a given selector. The ```defaultSelectorKey``` looks for a function name, then a match in the registry, and finally resorts to calling `toString` on the selector's ```resultFunc```.
200 |
201 | See the [tests](test/test.js#L246) for an alternate selectorKey.
202 |
203 |
204 | ### registerSelectors(keySelectorObj)
205 |
206 | Add a named selector to the graph. Set selector names as keys and selectors as values.
207 |
208 |
209 | ### Without The Extension
210 |
211 | If you're using an unsupported browser, or aren't happy with the extension, you can still get at the data.
212 |
213 | The dev tools bind to your app via this global:
214 | ```
215 | window.__RESELECT_TOOLS__ = {
216 | selectorGraph,
217 | checkSelector
218 | }
219 | ```
220 | Even without the devtools, you can call ```__RESELECT_TOOLS__.checkSelector('mySelector$')``` from the developer console or ```__RESLECT_TOOLS__.selectorGraph()``` to see what's going on. If the JSON output of the graph is hard to parse, there's an example of how to create a visual selector graph [here](tests/your-app.js).
221 |
222 |
223 | ## License
224 |
225 | MIT
226 |
227 | [build-badge]: https://api.travis-ci.org/skortchmark9/reselect-tools.svg?branch=master
228 | [build]: https://travis-ci.org/skortchmark9/reselect-tools
229 |
230 | [npm-badge]: https://img.shields.io/npm/v/reselect-tools.svg?style=flat-square
231 | [npm]: https://www.npmjs.org/package/reselect-tools
232 |
233 | [coveralls-badge]: https://coveralls.io/repos/github/skortchmark9/reselect-tools/badge.svg?branch=master
234 | [coveralls]: https://coveralls.io/github/skortchmark9/reselect-tools?branch=master
235 |
--------------------------------------------------------------------------------
/examples/demo-app.js:
--------------------------------------------------------------------------------
1 | var { selectorGraph } = ReselectTools;
2 | var { createSelector } = Reselect;
3 | ReselectTools.getStateWith(() => STORE);
4 |
5 | var STORE = {
6 | data: {
7 | users: {
8 | '1': {
9 | id: '1',
10 | name: 'bob',
11 | pets: ['a', 'b'],
12 | },
13 | '2': {
14 | id: '2',
15 | name: 'alice',
16 | pets: ['a'],
17 | }
18 | },
19 | pets: {
20 | 'a': {
21 | name: 'fluffy',
22 | },
23 | 'b': {
24 | name: 'paws',
25 | }
26 | }
27 | },
28 | ui: {
29 | currentUser: '1',
30 | }
31 | };
32 |
33 | const data$ = (state) => state.data;
34 | const ui$ = (state) => state.ui;
35 | var users$ = createSelector(data$, (data) => data.users);
36 | var pets$ = createSelector(data$, ({ pets }) => pets);
37 | var currentUser$ = createSelector(ui$, users$, (ui, users) => users[ui.currentUser]);
38 |
39 | var currentUserPets$ = createSelector(currentUser$, pets$, (currentUser, pets) => currentUser.pets.map((petId) => pets[petId]));
40 |
41 | const random$ = (state) => 1;
42 | const thingy$ = createSelector(random$, (number) => number + 1);
43 |
44 | const selectors = {
45 | data$,
46 | ui$,
47 | users$,
48 | pets$,
49 | currentUser$,
50 | currentUserPets$,
51 | random$,
52 | thingy$,
53 | };
54 |
55 | ReselectTools.registerSelectors(selectors);
56 |
57 |
58 | drawCytoscapeGraph(selectorGraph());
--------------------------------------------------------------------------------
/examples/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A Demo App Example
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/examples/extension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/examples/extension.png
--------------------------------------------------------------------------------
/examples/graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/examples/graph.png
--------------------------------------------------------------------------------
/examples/simple-graph.js:
--------------------------------------------------------------------------------
1 | var { checkSelector } = ReselectTools;
2 |
3 |
4 | const cytoDefaults = {
5 | style: [ // the stylesheet for the graph
6 | {
7 | selector: 'node',
8 | style: {
9 | 'background-color': '#666',
10 | 'label': 'data(id)'
11 | }
12 | },
13 |
14 | {
15 | selector: 'edge',
16 | style: {
17 | 'width': 3,
18 | 'line-color': '#ccc',
19 | 'target-arrow-color': '#ccc',
20 | 'target-arrow-shape': 'triangle'
21 | }
22 | }
23 | ],
24 |
25 | layout: {
26 | name: 'dagre',
27 | rankDir: 'BT',
28 | ranker: 'longest-path',
29 | }
30 | };
31 |
32 |
33 | function drawCytoscapeGraph(graph) {
34 | const { nodes, edges } = graph;
35 |
36 | const cytoNodes = Object.keys(nodes).map((name) => ({
37 | data: Object.assign({}, nodes[name], {
38 | id: name
39 | })
40 | }));
41 |
42 | const findSelectorId = (selector) => {
43 | const node = cytoNodes.find(({ data }) => data.name === selector);
44 | return node.data.id;
45 | };
46 |
47 | const cytoEdges = edges.map((edge, i) => ({data: {
48 | source: findSelectorId(edge.from),
49 | target: findSelectorId(edge.to),
50 | id: i,
51 | }}));
52 |
53 | const elements = cytoNodes.concat(cytoEdges);
54 |
55 | const cy = cytoscape(Object.assign({}, cytoDefaults, {
56 | container: document.getElementById('root'), // container to render in
57 | elements,
58 | }));
59 |
60 | cy.nodes().on("click", function(x, ...args) {
61 | const data = this.data();
62 | console.log(data.name, checkSelector(data.name));
63 | });
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/examples/your-app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Example Graph: (just add json)
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/examples/your-app.js:
--------------------------------------------------------------------------------
1 | // Comes from JSON.stringify(selectorGraph());
2 | const graph = {
3 | "edges": [
4 | {
5 | "from": "users$",
6 | "to": "data$"
7 | },
8 | {
9 | "from": "pets$",
10 | "to": "data$"
11 | },
12 | {
13 | "from": "currentUser$",
14 | "to": "ui$"
15 | },
16 | {
17 | "from": "currentUser$",
18 | "to": "users$"
19 | },
20 | {
21 | "from": "currentUserPets$",
22 | "to": "currentUser$"
23 | },
24 | {
25 | "from": "currentUserPets$",
26 | "to": "pets$"
27 | },
28 | {
29 | "from": "thingy$",
30 | "to": "random$"
31 | }
32 | ],
33 | "nodes": {
34 | "currentUser$": {
35 | "name": "currentUser$",
36 | "recomputations": 0
37 | },
38 | "currentUserPets$": {
39 | "name": "currentUserPets$",
40 | "recomputations": 0
41 | },
42 | "data$": {
43 | "name": "data$",
44 | "recomputations": null,
45 | },
46 | "pets$": {
47 | "name": "pets$",
48 | "recomputations": 0
49 | },
50 | "random$": {
51 | "name": "random$",
52 | "recomputations": null,
53 | },
54 | "thingy$": {
55 | "name": "thingy$",
56 | "recomputations": 0
57 | },
58 | "ui$": {
59 | "name": "ui$",
60 | "recomputations": null,
61 | },
62 | "users$": {
63 | "name": "users$",
64 | "recomputations": 0
65 | }
66 | }
67 | }
68 |
69 | // This overrides the graph the devtools get - don't try this at home!
70 | window.__RESELECT_TOOLS__.selectorGraph = () => graph;
71 |
72 |
73 | drawCytoscapeGraph(graph);
--------------------------------------------------------------------------------
/extension/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"],
3 | "plugins": ["add-module-exports", "transform-decorators-legacy", "transform-runtime"]
4 | }
5 |
--------------------------------------------------------------------------------
/extension/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "globals": {
5 | "chrome": true
6 | },
7 | "env": {
8 | "browser": true,
9 | "node": true
10 | },
11 | "rules": {
12 | "react/forbid-prop-types": 0,
13 | "react/prefer-stateless-function": 0,
14 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
15 | "jsx-a11y/no-static-element-interactions": 0,
16 | "jsx-a11y/label-has-for": 0,
17 | "consistent-return": 0,
18 | "comma-dangle": 0,
19 | "spaced-comment": 0,
20 | "global-require": 0
21 | },
22 | "plugins": [
23 | "react"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/extension/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | .DS_Store
4 |
5 | build/
6 | dev/
7 |
8 | *.zip
9 | *.crx
10 | *.pem
11 | update.xml
12 |
--------------------------------------------------------------------------------
/extension/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 | language: node_js
4 | node_js:
5 | - "6"
6 | - "7"
7 | cache:
8 | directories:
9 | - $HOME/.yarn-cache
10 | - node_modules
11 | env:
12 | - CXX=g++-4.8
13 | addons:
14 | apt:
15 | sources:
16 | - google-chrome
17 | - ubuntu-toolchain-r-test
18 | packages:
19 | - google-chrome-stable
20 | - g++-4.8
21 |
22 | install:
23 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
24 | - npm install -g yarn
25 | - yarn install
26 |
27 | before_script:
28 | - export DISPLAY=:99.0
29 | - sh -e /etc/init.d/xvfb start &
30 | - sleep 3
31 |
32 | script:
33 | - yarn run lint
34 | - yarn test
35 | - yarn run build
36 | - yarn run test-e2e
37 |
--------------------------------------------------------------------------------
/extension/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Jhen-Jie Hong
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/extension/README.md:
--------------------------------------------------------------------------------
1 | # Reselect Devtools Extension
2 |
3 | > Chrome Extension for debugging Reselect. Used with [reselect-tools](https://github.com/skortchmark9/reselect-tools)
4 |
5 | 
6 |
7 |
8 | ## Installation
9 |
10 | Your app must be using [reselect-tools](https://github.com/skortchmark9/reselect-tools) for this to work.
11 |
12 | - from [Chrome Web Store](https://chrome.google.com/webstore/detail/reselect-devtools/cjmaipngmabglflfeepmdiffcijhjlbb);
13 | - or build it with `npm i && npm run build` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./build/extension`;
14 | - or run it in dev mode with `npm i && npm run dev` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./dev`.
15 |
16 | ## Development
17 |
18 | * Run script
19 | ```bash
20 | # build files to './dev'
21 | # start webpack development server
22 | $ npm run dev
23 | ```
24 | * If you're developing Inject page, please allow `https://localhost:3000` connections. (Because `injectpage` injected GitHub (https) pages, so webpack server procotol must be https.)
25 | * [Load unpacked extensions](https://developer.chrome.com/extensions/getstarted#unpacked) with `./dev` folder.
26 |
27 | ## Caveat
28 |
29 | This is really just an MVP - I still need to write tests and remove cruft from the original [boilerplate](https://github.com/jhen0409/react-chrome-extension-boilerplate). However, its companion library is ready, so I want to get that off the ground and then I'll publish updates to this and productionize it better.
30 |
31 | ## Boilerplate Cruft
32 |
33 | #### React/Redux hot reload
34 |
35 | This boilerplate uses `Webpack` and `react-transform`, and use `Redux`. You can hot reload by editing related files of Popup & Window & Inject page.
36 |
37 | ### Build
38 |
39 | ```bash
40 | # build files to './build'
41 | $ npm run build
42 | ```
43 |
44 | ### Compress
45 |
46 | ```bash
47 | # compress build folder to {manifest.name}.zip and crx
48 | $ npm run build
49 | $ npm run compress -- [options]
50 | ```
51 |
52 | #### Options
53 |
54 | If you want to build `crx` file (auto update), please provide options, and add `update.xml` file url in [manifest.json](https://developer.chrome.com/extensions/autoupdate#update_url manifest.json).
55 |
56 | * --app-id: your extension id (can be get it when you first release extension)
57 | * --key: your private key path (default: './key.pem')
58 | you can use `npm run compress-keygen` to generate private key `./key.pem`
59 | * --codebase: your `crx` file url
60 |
61 | See [autoupdate guide](https://developer.chrome.com/extensions/autoupdate) for more information.
62 |
63 | ### Test
64 |
65 | * `test/app`: React components, Redux actions & reducers tests
66 | * `test/e2e`: E2E tests (use [chromedriver](https://www.npmjs.com/package/chromedriver), [selenium-webdriver](https://www.npmjs.com/package/selenium-webdriver))
67 |
68 | ```bash
69 | # lint
70 | $ npm run lint
71 | # test/app
72 | $ npm test
73 | $ npm test -- --watch # watch files
74 | # test/e2e
75 | $ npm run build
76 | $ npm run test-e2e
77 | ```
78 |
79 | ## LICENSE
80 |
81 | [MIT](LICENSE)
82 |
--------------------------------------------------------------------------------
/extension/Screen Shot 2017-11-15 at 3.34.11 AM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/extension/Screen Shot 2017-11-15 at 3.34.11 AM.png
--------------------------------------------------------------------------------
/extension/TODO.md:
--------------------------------------------------------------------------------
1 | Features:
2 | - [X] refresh button?
3 | - [X] make the graph pretty
4 | - [X] highlight dependencies through graph
5 | - [X] display number of recomputations (might need a refresh button)
6 | - [X] deal with long labels
7 | - [X] deal w/weird flexbox layout issues
8 | - [X] change N/A/ everywhere
9 | - [X] should we be able to get the value of selectors in the extension that we haven't added. - no
10 | - [X] pageApi can run into problems because we attempt to pass in an unregistered func with quotes in it.
11 | - [X] inject id field into graph nodes
12 | - [X] search selectors
13 | - [X] lock drawer open or closed
14 | - [X] find nodes depend on a given node
15 | - [X] highlight most recomputed nodes
16 | - [X] improve checkSelector rendering - zip inputs and dependencies
17 | - [ ] highlight unregistered nodes
18 | - [ ] show page action only when we are on a page w/devtools on it
19 | - [ ] Enable / disable depending on whether or not reselect tools have been installed
20 |
21 |
22 | Platforms:
23 | - [ ] Allow remote debugging with an RPC interface a la redux devtools
24 |
25 | Productionize
26 | - [X] Create icon
27 | - [X] Remove todoapp references
28 | - [X] Remove console logs
29 | - [X] Remove unnecessary pug templates
30 | - [X] Handle bad loads better
31 | - [X] Decide if we need redux
32 | - [ ] Remove all references to boilerplate
33 | - [ ] Set up linting, at least
34 | - [ ] At least look at the tests
35 |
--------------------------------------------------------------------------------
/extension/app/actions/graph.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes';
2 |
3 | export function uncheckSelector() {
4 | return { type: types.UNCHECK_SELECTOR };
5 | }
6 |
7 | export function checkSelectorFailed(selector) {
8 | return { type: types.CHECK_SELECTOR_FAILED, payload: { selector } };
9 | }
10 |
11 | export function checkSelectorSuccess(selector) {
12 | return { type: types.CHECK_SELECTOR_SUCCESS, payload: { selector } };
13 | }
14 |
15 | export function checkSelector(selector) {
16 | return { type: types.CHECK_SELECTOR, payload: { selector } };
17 | }
18 |
19 |
20 | export function getSelectorGraphFailed() {
21 | return { type: types.GET_SELECTOR_GRAPH_FAILED };
22 | }
23 |
24 | export function getSelectorGraphSuccess(graph) {
25 | return { type: types.GET_SELECTOR_GRAPH_SUCCESS, payload: { graph } };
26 | }
27 |
28 | export function getSelectorGraph() {
29 | return { type: types.GET_SELECTOR_GRAPH };
30 | }
31 |
--------------------------------------------------------------------------------
/extension/app/components/Dock.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import MdKeyboardArrowLeft from 'react-icons/lib/md/keyboard-arrow-left';
3 | import MdKeyboardArrowRight from 'react-icons/lib/md/keyboard-arrow-right';
4 | import Button from 'remotedev-app/lib/components/Button';
5 |
6 | const Subheader = ({ style, children, ...props }) => (
7 | {children}
8 | );
9 |
10 | const messageContainerStyle = {
11 | display: 'flex',
12 | justifyContent: 'space-between',
13 | alignItems: 'center',
14 | flexShrink: 0,
15 | };
16 |
17 | const Dock = ({ isOpen, toggleDock, message, children }) => {
18 | const dockStyle = {
19 | position: 'absolute',
20 | background: 'rgb(0, 43, 54)',
21 | top: 0,
22 | borderRight: '1px solid rgb(79, 90, 101)',
23 | borderTop: '1px solid rgb(79, 90, 101)',
24 | transform: `translateX(${isOpen ? 0 : '-100%'})`,
25 | left: 0,
26 | height: '100%',
27 | padding: '10px',
28 | minWidth: '220px',
29 | zIndex: 1,
30 | transition: 'transform 200ms ease-out',
31 | boxSizing: 'border-box',
32 | display: 'flex',
33 | flexDirection: 'column',
34 | };
35 | const showButtonStyle = {
36 | position: 'relative',
37 | right: 0,
38 | top: 0,
39 | transition: 'transform 200ms ease-out',
40 | transform: `translateX(${isOpen ? 0 : '100%'})`,
41 | };
42 | return (
43 |
44 |
45 | {message}
46 |
47 |
51 |
52 |
53 | {children}
54 |
55 | );
56 | };
57 |
58 | Dock.propTypes = {
59 | isOpen: PropTypes.bool.isRequired,
60 | toggleDock: PropTypes.func,
61 | children: PropTypes.object,
62 | message: PropTypes.string
63 | };
64 |
65 | export default Dock;
66 |
--------------------------------------------------------------------------------
/extension/app/components/Footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | color: #777;
3 | padding: 10px 15px;
4 | height: 20px;
5 | text-align: center;
6 | border-top: 1px solid #e6e6e6;
7 | }
8 |
9 | .footer:before {
10 | content: '';
11 | position: absolute;
12 | right: 0;
13 | bottom: 0;
14 | left: 0;
15 | height: 50px;
16 | overflow: hidden;
17 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
18 | 0 8px 0 -3px #f6f6f6,
19 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
20 | 0 16px 0 -6px #f6f6f6,
21 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
22 | }
23 |
24 | .filters {
25 | margin: 0;
26 | padding: 0;
27 | list-style: none;
28 | position: absolute;
29 | right: 0;
30 | left: 0;
31 | }
32 |
33 | .filters li {
34 | display: inline;
35 | }
36 |
37 | .filters li a {
38 | color: inherit;
39 | margin: 3px;
40 | padding: 3px 7px;
41 | text-decoration: none;
42 | border: 1px solid transparent;
43 | border-radius: 3px;
44 | }
45 |
46 | .filters li a.selected,
47 | .filters li a:hover {
48 | border-color: rgba(175, 47, 47, 0.1);
49 | }
50 |
51 | .filters li a.selected {
52 | border-color: rgba(175, 47, 47, 0.2);
53 | }
54 |
55 | .todoCount {
56 | float: left;
57 | text-align: left;
58 | }
59 |
60 | .todoCount strong {
61 | font-weight: 300;
62 | }
63 |
64 | .clearCompleted,
65 | html .clearCompleted:active {
66 | float: right;
67 | position: relative;
68 | line-height: 20px;
69 | text-decoration: none;
70 | cursor: pointer;
71 | }
72 |
73 | .clearCompleted:hover {
74 | text-decoration: underline;
75 | }
76 |
77 | @media (max-width: 430px) {
78 | .footer {
79 | height: 50px;
80 | }
81 |
82 | .filters {
83 | bottom: 10px;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/extension/app/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import classnames from 'classnames';
3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters';
4 | import style from './Footer.css';
5 |
6 | const FILTERS = [SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED];
7 | const FILTER_TITLES = {
8 | [SHOW_ALL]: 'All',
9 | [SHOW_ACTIVE]: 'Active',
10 | [SHOW_COMPLETED]: 'Completed'
11 | };
12 |
13 | export default class Footer extends Component {
14 |
15 | static propTypes = {
16 | completedCount: PropTypes.number.isRequired,
17 | activeCount: PropTypes.number.isRequired,
18 | filter: PropTypes.string.isRequired,
19 | onClearCompleted: PropTypes.func.isRequired,
20 | onShow: PropTypes.func.isRequired
21 | };
22 |
23 | constructor(props, context) {
24 | super(props, context);
25 | if (props.onShow) {
26 | this.filterHandlers = FILTERS.map(filter => () => props.onShow(filter));
27 | }
28 | }
29 |
30 | componentWillReceiveProps(nextProps) {
31 | if (nextProps.onShow) {
32 | this.filterHandlers = FILTERS.map(filter => () => nextProps.onShow(filter));
33 | }
34 | }
35 |
36 | renderTodoCount() {
37 | const { activeCount } = this.props;
38 | const itemWord = activeCount === 1 ? 'item' : 'items';
39 |
40 | return (
41 |
42 | {activeCount || 'No'} {itemWord} left
43 |
44 | );
45 | }
46 |
47 | renderFilterLink(filter, handler) {
48 | const title = FILTER_TITLES[filter];
49 | const { filter: selectedFilter } = this.props;
50 |
51 | return (
52 |
57 | {title}
58 |
59 | );
60 | }
61 |
62 | renderClearButton() {
63 | const { completedCount, onClearCompleted } = this.props;
64 | if (completedCount > 0) {
65 | return (
66 |
72 | );
73 | }
74 | }
75 |
76 | render() {
77 | return (
78 |
89 | );
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/extension/app/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import Button from 'remotedev-app/lib/components/Button';
3 | import MdHelp from 'react-icons/lib/md/help';
4 | import FindReplace from 'react-icons/lib/md/find-replace';
5 | import RefreshIcon from 'react-icons/lib/md/refresh';
6 | import styles from 'remotedev-app/lib/styles';
7 |
8 |
9 | const headerStyles = {
10 | refreshButton: {
11 | position: 'absolute',
12 | top: 0,
13 | left: 0,
14 | },
15 | select: {
16 | background: 'none',
17 | border: 'none',
18 | color: 'white',
19 | outline: 'none',
20 | },
21 | };
22 |
23 | class NumberButton extends Component {
24 | static propTypes = {
25 | defaultValue: PropTypes.number,
26 | onClick: PropTypes.func,
27 | numbers: PropTypes.array.isRequired,
28 | }
29 | constructor(props) {
30 | super(props);
31 | const value = props.defaultValue === undefined ? 1 : props.defaultValue;
32 | this.state = { value: value.toString() };
33 | this.onNumberChange = this.onNumberChange.bind(this);
34 | this.onClickWithNumber = this.onClickWithNumber.bind(this);
35 | }
36 | onNumberChange(e) {
37 | this.setState({ value: e.target.value.toString() });
38 | e.stopPropagation();
39 | }
40 | onClickWithNumber(e) {
41 | this.props.onClick(parseInt(this.state.value, 10));
42 | }
43 | stopPropagation(e) {
44 | e.stopPropagation();
45 | }
46 | render() {
47 | const { numbers, children, ...other } = this.props;
48 | const { value } = this.state;
49 | const options = numbers.map(n => );
50 |
51 | return (
52 |
66 | );
67 | }
68 | }
69 |
70 |
71 | export default function Header({ onRefresh, onHelp, onPaintWorst }) {
72 | return (
73 |
74 |
79 |
83 |
88 | Select
89 | Most Recomputed
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | Header.propTypes = {
97 | onRefresh: PropTypes.func,
98 | onHelp: PropTypes.func,
99 | onPaintWorst: PropTypes.func,
100 | };
101 |
--------------------------------------------------------------------------------
/extension/app/components/MainSection.css:
--------------------------------------------------------------------------------
1 | .main {
2 | position: relative;
3 | z-index: 2;
4 | border-top: 1px solid #e6e6e6;
5 | }
6 |
7 | .todoList {
8 | margin: 0;
9 | padding: 0;
10 | list-style: none;
11 | }
12 |
13 | .todoList li {
14 | position: relative;
15 | font-size: 24px;
16 | border-bottom: 1px solid #ededed;
17 | }
18 |
19 | .todoList li:last-child {
20 | border-bottom: none;
21 | }
22 |
23 | .todoList li.editing {
24 | border-bottom: none;
25 | padding: 0;
26 | }
27 |
28 | .todoList li.editing .edit {
29 | display: block;
30 | width: 506px;
31 | padding: 13px 17px 12px 17px;
32 | margin: 0 0 0 43px;
33 | }
34 |
35 | .todoList li.editing .view {
36 | display: none;
37 | }
38 |
39 | .todoList li .toggle {
40 | text-align: center;
41 | width: 40px;
42 | /* auto, since non-WebKit browsers doesn't support input styling */
43 | height: auto;
44 | position: absolute;
45 | top: 0;
46 | bottom: 0;
47 | margin: auto 0;
48 | border: none; /* Mobile Safari */
49 | -webkit-appearance: none;
50 | -moz-appearance: none;
51 | }
52 |
53 | .todoList li .toggle:after {
54 | content: url('data:image/svg+xml;utf8,');
55 | }
56 |
57 | .todoList li .toggle:checked:after {
58 | content: url('data:image/svg+xml;utf8,');
59 | }
60 |
61 | .todoList li label {
62 | white-space: pre-line;
63 | word-break: break-all;
64 | padding: 15px 60px 15px 15px;
65 | margin-left: 45px;
66 | display: block;
67 | line-height: 1.2;
68 | transition: color 0.4s;
69 | }
70 |
71 | .todoList li.completed label {
72 | color: #d9d9d9;
73 | text-decoration: line-through;
74 | }
75 |
76 | .todoList li .destroy {
77 | display: none;
78 | position: absolute;
79 | top: 0;
80 | right: 10px;
81 | bottom: 0;
82 | width: 40px;
83 | height: 40px;
84 | margin: auto 0;
85 | font-size: 30px;
86 | color: #cc9a9a;
87 | margin-bottom: 11px;
88 | transition: color 0.2s ease-out;
89 | }
90 |
91 | .todoList li .destroy:hover {
92 | color: #af5b5e;
93 | }
94 |
95 | .todoList li .destroy:after {
96 | content: '×';
97 | }
98 |
99 | .todoList li:hover .destroy {
100 | display: block;
101 | }
102 |
103 | .todoList li .edit {
104 | display: none;
105 | }
106 |
107 | .todoList li.editing:last-child {
108 | margin-bottom: -1px;
109 | }
110 |
111 | .toggleAll {
112 | position: absolute;
113 | top: -55px;
114 | left: -12px;
115 | width: 60px;
116 | height: 34px;
117 | text-align: center;
118 | border: none; /* Mobile Safari */
119 | }
120 |
121 | .toggleAll:before {
122 | content: '❯';
123 | font-size: 22px;
124 | color: #e6e6e6;
125 | padding: 10px 27px 10px 27px;
126 | }
127 |
128 | .toggleAll:checked:before {
129 | color: #737373;
130 | }
131 |
132 | /*
133 | Hack to remove background from Mobile Safari.
134 | Can't use it globally since it destroys checkboxes in Firefox
135 | */
136 | @media screen and (-webkit-min-device-pixel-ratio:0) {
137 | .toggleAll,
138 | .todoList li .toggle {
139 | background: none;
140 | }
141 |
142 | .todoList li .toggle {
143 | height: 40px;
144 | }
145 |
146 | .toggleAll {
147 | transform: rotate(90deg);
148 | -webkit-appearance: none;
149 | -moz-appearance: none;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/extension/app/components/MainSection.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import TodoItem from './TodoItem';
3 | import Footer from './Footer';
4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters';
5 | import style from './MainSection.css';
6 |
7 | const TODO_FILTERS = {
8 | [SHOW_ALL]: () => true,
9 | [SHOW_ACTIVE]: todo => !todo.completed,
10 | [SHOW_COMPLETED]: todo => todo.completed
11 | };
12 |
13 | export default class MainSection extends Component {
14 |
15 | static propTypes = {
16 | todos: PropTypes.array.isRequired,
17 | actions: PropTypes.object.isRequired
18 | };
19 |
20 | constructor(props, context) {
21 | super(props, context);
22 | this.state = { filter: SHOW_ALL };
23 | }
24 |
25 | handleClearCompleted = () => {
26 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed);
27 | if (atLeastOneCompleted) {
28 | this.props.actions.clearCompleted();
29 | }
30 | };
31 |
32 | handleShow = (filter) => {
33 | this.setState({ filter });
34 | };
35 |
36 | renderToggleAll(completedCount) {
37 | const { todos, actions } = this.props;
38 | if (todos.length > 0) {
39 | return (
40 |
46 | );
47 | }
48 | }
49 |
50 | renderFooter(completedCount) {
51 | const { todos } = this.props;
52 | const { filter } = this.state;
53 | const activeCount = todos.length - completedCount;
54 |
55 | if (todos.length) {
56 | return (
57 |
64 | );
65 | }
66 | }
67 |
68 | render() {
69 | const { todos, actions } = this.props;
70 | const { filter } = this.state;
71 |
72 | const filteredTodos = todos.filter(TODO_FILTERS[filter]);
73 | const completedCount = todos.reduce(
74 | (count, todo) => (todo.completed ? count + 1 : count),
75 | 0
76 | );
77 |
78 | return (
79 |
80 | {this.renderToggleAll(completedCount)}
81 |
82 | {filteredTodos.map(todo =>
83 |
84 | )}
85 |
86 | {this.renderFooter(completedCount)}
87 |
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/extension/app/components/SelectorGraph.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import cytoscape from 'cytoscape';
3 | import dagre from 'cytoscape-dagre';
4 |
5 | cytoscape.use(dagre);
6 |
7 | const truncateText = (str, maxChars = 20) => (str.length > maxChars ? str.slice(0, maxChars) : str);
8 | const labelText = (id, recomputations) => truncateText(id) + (recomputations === null ? '' : ` (${recomputations})`);
9 |
10 |
11 | const colors = {
12 | defaultEdge: 'rgb(79, 90, 101)',
13 | defaultNodeLabel: 'rgb(111, 179, 210)',
14 | defaultNode: 'rgb(232, 234, 246)',
15 | selectedNode: 'orange',
16 | dependency: '#ffeb3b',
17 | dependent: '#f868d0',
18 | recomputed: 'red',
19 | };
20 |
21 | const defaultEdgeStyle = {
22 | 'curve-style': 'bezier',
23 | width: 4,
24 | 'target-arrow-shape': 'triangle',
25 | 'line-color': colors.defaultEdge,
26 | 'target-arrow-color': colors.defaultEdge,
27 | 'z-index': 1,
28 | };
29 |
30 | const selectedNodeStyle = {
31 | 'background-color': colors.selectedNode
32 | };
33 |
34 | const defaultNodeStyle = {
35 | label: 'data(label)',
36 | color: colors.defaultNodeLabel,
37 | 'background-color': colors.defaultNode,
38 | };
39 |
40 | const Y_SPACING = 0.1;
41 |
42 | const cytoDefaults = {
43 | style: [
44 | {
45 | selector: 'edge',
46 | style: defaultEdgeStyle
47 | },
48 | {
49 | selector: 'node',
50 | style: defaultNodeStyle
51 | }
52 | ],
53 | layout: {
54 | name: 'dagre',
55 | rankDir: 'BT',
56 | // fit: false,
57 | ranker: 'longest-path',
58 | // padding: 0,
59 | nodeDimensionsIncludeLabels: false, // this doesn't really work alas
60 | transform: (node, { x, y }) => {
61 | // increase distance between y ranks, and offset some nodes
62 | // a bit up and down so labels don't collide.
63 | const offsetDirection = Math.random() > 0.5 ? 1 : -1;
64 | const offset = y * Y_SPACING;
65 | return { x, y: y + offset + (offset * offsetDirection) };
66 | }
67 | }
68 | };
69 |
70 | function createCytoElements(nodes, edges) {
71 | const cytoNodes = Object.keys(nodes).map(name => ({
72 | data: Object.assign({}, nodes[name], {
73 | id: name,
74 | label: labelText(name, nodes[name].recomputations)
75 | }),
76 | }));
77 |
78 |
79 | const cytoEdges = edges.map((edge, i) => ({ data: {
80 | source: edge.from,
81 | target: edge.to,
82 | id: i,
83 | } }));
84 |
85 | return cytoNodes.concat(cytoEdges);
86 | }
87 |
88 |
89 | export function drawCytoscapeGraph(container, nodes, edges) {
90 | const elements = createCytoElements(nodes, edges);
91 | return cytoscape({ ...cytoDefaults, container, elements });
92 | }
93 |
94 | function paintDependencies(elts) {
95 | elts.forEach((elt) => {
96 | if (elt.isNode()) {
97 | elt.style({
98 | 'background-color': colors.dependency,
99 | });
100 | } else if (elt.isEdge()) {
101 | elt.style({
102 | 'line-color': colors.dependency,
103 | 'z-index': 99,
104 | 'target-arrow-color': colors.dependency,
105 | });
106 | }
107 | });
108 | }
109 |
110 | function paintDependents(elts) {
111 | elts.forEach((elt) => {
112 | if (elt.isNode()) {
113 | elt.style({
114 | 'background-color': colors.dependent,
115 | });
116 | } else if (elt.isEdge()) {
117 | elt.style({
118 | 'line-color': colors.dependent,
119 | 'z-index': 99,
120 | 'target-arrow-color': colors.dependent,
121 | });
122 | }
123 | });
124 | }
125 |
126 |
127 | const circleStyle = {
128 | borderRadius: '50%',
129 | width: '1em',
130 | height: '1em',
131 | display: 'inline-block',
132 | margin: '0 0.5em',
133 | marginTop: '0.2em'
134 | };
135 | const Circle = ({ color }) => ;
136 | const LegendItem = ({ name, color }) => (
137 |
138 | {name}
139 |
140 | );
141 |
142 |
143 | export default class SelectorGraph extends Component {
144 | static propTypes = {
145 | nodes: PropTypes.object.isRequired,
146 | edges: PropTypes.array.isRequired,
147 | checkSelector: PropTypes.func,
148 | selector: PropTypes.object,
149 | };
150 |
151 | componentDidMount() {
152 | this.cy = drawCytoscapeGraph(this.cyElement, this.props.nodes, this.props.edges);
153 | const pan = this.cy.pan();
154 | const height = this.cy.height();
155 | this.cy.pan({ ...pan, y: height / 3 });
156 | this.bindEvents();
157 | }
158 |
159 | componentWillReceiveProps(nextProps) {
160 | if (nextProps.nodes !== this.props.nodes || nextProps.edges !== this.props.edges) {
161 | const { nodes, edges } = nextProps;
162 | const elements = createCytoElements(nodes, edges);
163 | this.cy.json({ elements });
164 | }
165 |
166 | if (nextProps.selector && nextProps.selector !== this.props.selector) {
167 | this.paintNodeSelection(nextProps.selector);
168 | }
169 | }
170 |
171 | shouldComponentUpdate() {
172 | return false;
173 | }
174 |
175 | componentWillUnmount() {
176 | if (this.cy) this.cy.destroy();
177 | }
178 |
179 | reset() {
180 | const { label, ...nodeStyle } = defaultNodeStyle; // eslint-disable-line no-unused-vars
181 | this.cy.nodes().style(nodeStyle);
182 | this.cy.edges().style(defaultEdgeStyle);
183 | }
184 |
185 | paintNodeSelection(selector) {
186 | this.reset();
187 | if (!selector || !selector.id) return;
188 |
189 | // can't search with selectors because special chars, i.e. $ interfere
190 | const selectedNode = this.cy.nodes(node => node.data().id === selector.id);
191 | selectedNode.style(selectedNodeStyle);
192 | paintDependencies(selectedNode.successors());
193 | paintDependents(selectedNode.predecessors());
194 | }
195 |
196 | highlightNMostRecomputed(n = 1) {
197 | this.reset();
198 | const nodes = this.cy.nodes();
199 | const recomputationBuckets = new Map(); // bucketzzz
200 | nodes.forEach((node) => {
201 | const recomputations = node.data().recomputations;
202 | if (!recomputationBuckets.get(recomputations)) {
203 | recomputationBuckets.set(recomputations, []);
204 | }
205 | recomputationBuckets.get(recomputations).push(node);
206 | });
207 | const mostRecomputed = [...recomputationBuckets.keys()].sort((x, y) => x - y);
208 | const nMost = mostRecomputed.slice(-n);
209 | const highlighted = nMost.reduce((acc, key) => acc.concat(recomputationBuckets.get(key)), []);
210 | highlighted.forEach(node => node.style({
211 | 'background-color': colors.recomputed,
212 | }));
213 | }
214 |
215 | bindEvents() {
216 | const { checkSelector } = this.props;
217 | function clickHandler() {
218 | const data = this.data();
219 | checkSelector(data);
220 | }
221 | if (checkSelector) {
222 | this.cy.on('tap', 'node', clickHandler);
223 | }
224 | }
225 |
226 | render() {
227 | const legendStyle = {
228 | position: 'absolute',
229 | top: 0,
230 | right: 0,
231 | zIndex: 100,
232 | };
233 | return (
234 | { this.cyElement = e; }}>
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 | );
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/extension/app/components/SelectorInspector.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | import Search from './SelectorSearch';
4 |
5 | const hStyle = {
6 | overflow: 'hidden',
7 | textOverflow: 'ellipsis',
8 | margin: 0,
9 | marginRight: '10px',
10 | flexWrap: 'nowrap',
11 | whiteSpace: 'nowrap',
12 | };
13 |
14 | const containerStyle = {
15 | flexShrink: 0,
16 | overflowX: 'hidden',
17 | overflowY: 'auto',
18 | borderBottomWidth: '3px',
19 | borderBottomStyle: 'double',
20 | display: 'flex',
21 | flexDirection: 'row',
22 | alignItems: 'center',
23 | border: '1px solid rgb(79, 90, 101)',
24 | padding: '10px',
25 | };
26 |
27 | function SelectorInfo({ selector }) {
28 | const { recomputations, isNamed, name } = selector;
29 |
30 | const subheadStyle = { ...hStyle, color: 'rgb(111, 179, 210)' };
31 | let message = `(${recomputations} recomputations)`;
32 | if (recomputations === null) {
33 | message = '(not memoized)';
34 | }
35 |
36 | return (
37 |
38 |
{name}
39 |
40 |
{message}
41 | { !isNamed && (unregistered)
}
42 |
43 |
44 | );
45 | }
46 | SelectorInfo.propTypes = { selector: PropTypes.object };
47 |
48 | export default class SelectorInspector extends Component {
49 | static propTypes = {
50 | selector: PropTypes.object,
51 | selectors: PropTypes.object,
52 | onSelectorChosen: PropTypes.func.isRequired,
53 | }
54 |
55 | constructor(props) {
56 | super(props);
57 | this.state = {
58 | searching: false,
59 | };
60 | this.toggleSearch = this.toggleSearch.bind(this);
61 | }
62 |
63 | toggleSearch() {
64 | this.setState({ searching: !this.state.searching });
65 | }
66 |
67 | render() {
68 | const { selector, selectors, onSelectorChosen } = this.props;
69 | const { searching } = this.state;
70 | return (
71 |
72 | { selector ?
73 | :
Choose a selector
74 | }
75 |
81 |
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/extension/app/components/SelectorSearch.css:
--------------------------------------------------------------------------------
1 | .searchContainer {
2 | flex-shrink: 0;
3 | display: flex;
4 | align-items: center;
5 | flex-grow: 1;
6 | max-width: 800px;
7 | margin-left: auto;
8 | justify-content: flex-end;
9 | }
10 |
11 | .searchContainer > a {
12 | flex-grow: 0 !important;
13 | margin-left: 1em !important;
14 | }
15 |
16 | .searchContainer .autocomplete {
17 | flex-grow: 1;
18 | display: flex;
19 | z-index: 4;
20 | }
21 |
22 | .searchContainer .autocomplete input {
23 | min-width: 200px;
24 | flex-grow: 1;
25 | border: 0;
26 | padding: 0.3em;
27 | outline: none;
28 | background: none;
29 | color: white;
30 | border-bottom: 2px solid rgb(79, 90, 101);
31 | }
32 |
33 | .searchContainer .autocomplete input:focus {
34 | border-color: #e8eaf7
35 | }
--------------------------------------------------------------------------------
/extension/app/components/SelectorSearch.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import Autocomplete from 'react-autocomplete';
3 | import Button from 'remotedev-app/lib/components/Button';
4 | import MdSearch from 'react-icons/lib/md/search';
5 |
6 | import style from './SelectorSearch.css';
7 |
8 | const itemStyle = isHighlighted => ({
9 | padding: '1em',
10 | background: isHighlighted ? 'rgb(79, 90, 101)' : 'rgb(0, 43, 55)',
11 | borderBottom: '1px solid rgb(79, 90, 101)'
12 | });
13 |
14 | const renderItem = (item, isHighlighted) => (
15 |
16 | {item.id}
17 |
18 | );
19 |
20 | const getItemValue = item => item.id;
21 |
22 | export default class Search extends Component {
23 | static propTypes = {
24 | searching: PropTypes.bool,
25 | onToggleSearch: PropTypes.func.isRequired,
26 | selectors: PropTypes.object.isRequired,
27 | onSelectorChosen: PropTypes.func.isRequired,
28 | }
29 | constructor(props) {
30 | super(props);
31 | this.state = { value: '' };
32 | this.onInput = this.onInput.bind(this);
33 | this.onSelect = this.onSelect.bind(this);
34 | }
35 | componentWillReceiveProps(nextProps) {
36 | if (this.props.searching && !nextProps.searching) {
37 | this.setState({ value: '' });
38 | }
39 | }
40 | onInput(e) {
41 | this.setState({ value: e.target.value });
42 | }
43 | onSelect(id) {
44 | this.setState({ value: id });
45 | const { selectors, onSelectorChosen } = this.props;
46 | onSelectorChosen(selectors[id]);
47 | }
48 | render() {
49 | const { searching, onToggleSearch, selectors } = this.props;
50 | const { value } = this.state;
51 | const items = Object.keys(selectors).map(key => selectors[key]);
52 | const autocompleteProps = {
53 | className: style.autocomplete,
54 | style: {} // disable default inline styles
55 | };
56 |
57 | const suggestionContainerStyles = {
58 | boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
59 | fontSize: '90%',
60 | position: 'fixed',
61 | overflow: 'auto',
62 | maxHeight: '50%',
63 | };
64 |
65 | return (
66 |
67 | {searching &&
item.id.toLowerCase().indexOf(val.toLowerCase()) > -1}
74 | getItemValue={getItemValue}
75 | renderItem={renderItem}
76 | onChange={this.onInput}
77 | onSelect={this.onSelect}
78 | value={value}
79 | />
80 | }
81 |
84 |
85 | );
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/extension/app/components/SelectorState.css:
--------------------------------------------------------------------------------
1 | td {
2 | vertical-align: baseline;
3 | padding: 5px;
4 | padding-left: 0;
5 | border-bottom: 1px solid rgb(79, 90, 101);
6 | }
7 |
8 | tr {
9 | min-height: 20px;
10 | }
11 |
12 | section tr + tr {
13 | border-bottom: 1px solid rgb(79, 90, 101);
14 | }
15 |
16 | section h5 {
17 | border-bottom: 1px solid white;
18 | }
--------------------------------------------------------------------------------
/extension/app/components/SelectorState.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import StateTree from './StateTree';
3 | import style from './SelectorState.css';
4 |
5 | const InputsSection = ({ zipped = [], onClickSelector }) => (
6 |
7 | {zipped.length ? 'Inputs' : 'No Inputs'}
8 |
20 |
21 | );
22 | InputsSection.propTypes = {
23 | zipped: PropTypes.array,
24 | onClickSelector: PropTypes.func
25 | };
26 |
27 | const OutputSection = ({ output }) => (
28 |
32 | );
33 | OutputSection.propTypes = { output: PropTypes.any };
34 |
35 | export default class SelectorState extends Component {
36 | static propTypes = {
37 | checkedSelector: PropTypes.object,
38 | onClickSelector: PropTypes.func
39 | }
40 | render() {
41 | const { checkedSelector, onClickSelector } = this.props;
42 | const { zipped, output } = checkedSelector;
43 | return (
44 |
45 |
46 | { zipped && }
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/extension/app/components/StateTree.js:
--------------------------------------------------------------------------------
1 | import JSONTree from 'react-json-tree';
2 | import React, { PropTypes } from 'react';
3 |
4 | const shouldExpandNode = (keyName, data, level) => false;
5 |
6 | const isObject = o => typeof o === 'object';
7 |
8 | const valueStyle = {
9 | marginLeft: '0.875em',
10 | paddingLeft: '1.25em',
11 | paddingTop: '0.25em',
12 | };
13 |
14 | const StateTree = ({ data, style = {} }) => (
15 |
16 | { isObject(data) ?
17 |
: { "" + data }
21 | }
22 |
23 | );
24 |
25 | StateTree.propTypes = {
26 | data: PropTypes.any,
27 | style: PropTypes.object,
28 | };
29 |
30 | export default StateTree;
31 |
--------------------------------------------------------------------------------
/extension/app/components/TodoItem.css:
--------------------------------------------------------------------------------
1 | .normal .toggle {
2 | text-align: center;
3 | width: 40px;
4 | /* auto, since non-WebKit browsers doesn't support input styling */
5 | height: auto;
6 | position: absolute;
7 | top: 0;
8 | bottom: 0;
9 | margin: auto 0;
10 | border: none; /* Mobile Safari */
11 | -webkit-appearance: none;
12 | -moz-appearance: none;
13 | }
14 |
15 | .normal .toggle:after {
16 | content: url('data:image/svg+xml;utf8,');
17 | }
18 |
19 | .normal .toggle:checked:after {
20 | content: url('data:image/svg+xml;utf8,');
21 | }
22 |
23 | .normal label {
24 | white-space: pre-line;
25 | word-break: break-all;
26 | padding: 15px 60px 15px 15px;
27 | margin-left: 45px;
28 | display: block;
29 | line-height: 1.2;
30 | transition: color 0.4s;
31 | }
32 |
33 | .normal .destroy {
34 | display: none;
35 | position: absolute;
36 | top: 0;
37 | right: 10px;
38 | bottom: 0;
39 | width: 40px;
40 | height: 40px;
41 | margin: auto 0;
42 | font-size: 30px;
43 | color: #cc9a9a;
44 | margin-bottom: 11px;
45 | transition: color 0.2s ease-out;
46 | }
47 |
48 | .normal .destroy:hover {
49 | color: #af5b5e;
50 | }
51 |
52 | .normal .destroy:after {
53 | content: '×';
54 | }
55 |
56 | .normal:hover .destroy {
57 | display: block;
58 | }
59 |
60 | .normal .edit {
61 | display: none;
62 | }
63 |
64 | .editing {
65 | border-bottom: none;
66 | padding: 0;
67 | composes: normal;
68 | }
69 |
70 | .editing:last-child {
71 | margin-bottom: -1px;
72 | }
73 |
74 | .editing .edit {
75 | display: block;
76 | width: 506px;
77 | padding: 13px 17px 12px 17px;
78 | margin: 0 0 0 43px;
79 | }
80 |
81 | .editing .view {
82 | display: none;
83 | }
84 |
85 | .completed label {
86 | color: #d9d9d9;
87 | text-decoration: line-through;
88 | }
89 |
90 | /*
91 | Hack to remove background from Mobile Safari.
92 | Can't use it globally since it destroys checkboxes in Firefox
93 | */
94 | @media screen and (-webkit-min-device-pixel-ratio:0) {
95 | .normal .toggle {
96 | background: none;
97 | }
98 |
99 | .normal .toggle {
100 | height: 40px;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/extension/app/components/TodoItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classnames from 'classnames';
3 | import TodoTextInput from './TodoTextInput';
4 | import style from './TodoItem.css';
5 |
6 | export default class TodoItem extends Component {
7 |
8 | static propTypes = {
9 | todo: PropTypes.object.isRequired,
10 | editTodo: PropTypes.func.isRequired,
11 | deleteTodo: PropTypes.func.isRequired,
12 | completeTodo: PropTypes.func.isRequired
13 | };
14 |
15 | constructor(props, context) {
16 | super(props, context);
17 | this.state = {
18 | editing: false
19 | };
20 | }
21 |
22 | handleDoubleClick = () => {
23 | this.setState({ editing: true });
24 | };
25 |
26 | handleSave = (text) => {
27 | const { todo, deleteTodo, editTodo } = this.props;
28 | if (text.length === 0) {
29 | deleteTodo(todo.id);
30 | } else {
31 | editTodo(todo.id, text);
32 | }
33 | this.setState({ editing: false });
34 | };
35 |
36 | handleComplete = () => {
37 | const { todo, completeTodo } = this.props;
38 | completeTodo(todo.id);
39 | };
40 |
41 | handleDelete = () => {
42 | const { todo, deleteTodo } = this.props;
43 | deleteTodo(todo.id);
44 | };
45 |
46 | render() {
47 | const { todo } = this.props;
48 |
49 | let element;
50 | if (this.state.editing) {
51 | element = (
52 |
57 | );
58 | } else {
59 | element = (
60 |
61 |
67 |
70 |
74 |
75 | );
76 | }
77 |
78 | return (
79 |
86 | {element}
87 |
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/extension/app/components/TodoTextInput.css:
--------------------------------------------------------------------------------
1 | .new,
2 | .edit {
3 | position: relative;
4 | margin: 0;
5 | width: 100%;
6 | font-size: 24px;
7 | font-family: inherit;
8 | font-weight: inherit;
9 | line-height: 1.4em;
10 | border: 0;
11 | outline: none;
12 | color: inherit;
13 | padding: 6px;
14 | border: 1px solid #999;
15 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
16 | box-sizing: border-box;
17 | font-smoothing: antialiased;
18 | }
19 |
20 | .new {
21 | padding: 16px 16px 16px 60px;
22 | border: none;
23 | background: rgba(0, 0, 0, 0.003);
24 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
25 | }
26 |
--------------------------------------------------------------------------------
/extension/app/components/TodoTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classnames from 'classnames';
3 | import style from './TodoTextInput.css';
4 |
5 | export default class TodoTextInput extends Component {
6 |
7 | static propTypes = {
8 | onSave: PropTypes.func.isRequired,
9 | text: PropTypes.string,
10 | placeholder: PropTypes.string,
11 | editing: PropTypes.bool,
12 | newTodo: PropTypes.bool
13 | };
14 |
15 | constructor(props, context) {
16 | super(props, context);
17 | this.state = {
18 | text: this.props.text || ''
19 | };
20 | }
21 |
22 | handleSubmit = (evt) => {
23 | const text = evt.target.value.trim();
24 | if (evt.which === 13) {
25 | this.props.onSave(text);
26 | if (this.props.newTodo) {
27 | this.setState({ text: '' });
28 | }
29 | }
30 | };
31 |
32 | handleChange = (evt) => {
33 | this.setState({ text: evt.target.value });
34 | };
35 |
36 | handleBlur = (evt) => {
37 | if (!this.props.newTodo) {
38 | this.props.onSave(evt.target.value);
39 | }
40 | };
41 |
42 | render() {
43 | return (
44 |
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/extension/app/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO';
2 | export const DELETE_TODO = 'DELETE_TODO';
3 | export const EDIT_TODO = 'EDIT_TODO';
4 | export const COMPLETE_TODO = 'COMPLETE_TODO';
5 | export const COMPLETE_ALL = 'COMPLETE_ALL';
6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
7 |
8 | export const CHECK_SELECTOR = 'CHECK_SELECTOR';
9 | export const CHECK_SELECTOR_FAILED = 'CHECK_SELECTOR_FAILED';
10 | export const CHECK_SELECTOR_SUCCESS = 'CHECK_SELECTOR_SUCCESS';
11 | export const UNCHECK_SELECTOR = 'UNCHECK_SELECTOR';
12 |
13 |
14 | export const GET_SELECTOR_GRAPH = 'GET_SELECTOR_GRAPH';
15 | export const GET_SELECTOR_GRAPH_FAILED = 'GET_SELECTOR_GRAPH_FAILED';
16 | export const GET_SELECTOR_GRAPH_SUCCESS = 'GET_SELECTOR_GRAPH_SUCCESS';
17 |
--------------------------------------------------------------------------------
/extension/app/constants/TodoFilters.js:
--------------------------------------------------------------------------------
1 | export const SHOW_ALL = 'show_all';
2 | export const SHOW_COMPLETED = 'show_completed';
3 | export const SHOW_ACTIVE = 'show_active';
4 |
--------------------------------------------------------------------------------
/extension/app/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 |
5 | import styles from 'remotedev-app/lib/styles';
6 | import enhance from 'remotedev-app/lib/hoc';
7 |
8 | import SelectorInspector from '../components/SelectorInspector';
9 | import SelectorState from '../components/SelectorState';
10 | import SelectorGraph from '../components/SelectorGraph';
11 | import Dock from '../components/Dock';
12 | import Header from '../components/Header';
13 |
14 | import * as SelectorActions from '../actions/graph';
15 |
16 |
17 | const contentStyles = {
18 | content: {
19 | width: '100%',
20 | display: 'flex',
21 | flexDirection: 'column',
22 | height: '100%',
23 | },
24 | graph: {
25 | border: '1px solid rgb(79, 90, 101)',
26 | position: 'relative',
27 | height: '100%',
28 | minHeight: 0,
29 | },
30 | recomputations: {
31 | position: 'absolute',
32 | bottom: 0,
33 | right: 0,
34 | margin: 0,
35 | padding: '1em',
36 | }
37 | };
38 |
39 |
40 | function renderMessage(message) {
41 | const centerText = { margin: 'auto' };
42 | return (
43 |
44 |
{message}
45 |
46 | );
47 | }
48 |
49 |
50 | function openGitRepo() {
51 | const url = 'https://github.com/skortchmark9/reselect-devtools-extension';
52 | window.open(url, '_blank');
53 | }
54 |
55 | const checkedSelector$ = (state) => {
56 | const { checkedSelectorId, nodes, edges } = state.graph;
57 | const selector = nodes[checkedSelectorId];
58 | if (!selector) return;
59 |
60 | // this is a bit ugly because it relies on edges being in order.
61 | const dependencies = edges.filter(edge => edge.from === checkedSelectorId);
62 | const dependencyIds = dependencies.map(edge => edge.to);
63 |
64 | if (!selector.inputs) {
65 | return selector;
66 | }
67 |
68 | const { inputs } = selector;
69 | if (dependencyIds.length !== inputs.length) {
70 | console.error(`Uh oh, inputs and edges out of sync on ${checkedSelectorId}`);
71 | }
72 |
73 | const zipped = [];
74 | for (let i = 0; i < dependencyIds.length; i++) {
75 | zipped.push([dependencyIds[i], inputs[i]]);
76 | }
77 | return { ...selector, zipped };
78 | };
79 |
80 |
81 | const RecomputationsTotal = ({ nodes }) => {
82 | const nodeArr = Object.keys(nodes).map(k => nodes[k]);
83 | const total = nodeArr.reduce((acc, node) => acc + node.recomputations, 0);
84 | return {total} Recomputations
;
85 | };
86 |
87 | @connect(
88 | state => ({
89 | graph: state.graph,
90 | checkedSelector: checkedSelector$(state),
91 | }),
92 | dispatch => ({
93 | actions: bindActionCreators(SelectorActions, dispatch)
94 | })
95 | )
96 | @enhance
97 | export default class App extends Component {
98 | static propTypes = {
99 | actions: PropTypes.object.isRequired,
100 | graph: PropTypes.object,
101 | checkedSelector: PropTypes.object,
102 | };
103 |
104 | constructor(props) {
105 | super(props);
106 | this.state = {
107 | dockIsOpen: true,
108 | };
109 | this.handleCheckSelector = this.handleCheckSelector.bind(this);
110 | this.refreshGraph = this.refreshGraph.bind(this);
111 | this.toggleDock = this.toggleDock.bind(this);
112 | this.paintNWorst = this.paintNWorst.bind(this);
113 | }
114 |
115 | componentDidMount() {
116 | this.refreshGraph();
117 | }
118 |
119 | refreshGraph() {
120 | this.sg && this.sg.reset();
121 | this.resetSelectorData();
122 | this.props.actions.getSelectorGraph();
123 | }
124 |
125 | paintNWorst(n) {
126 | this.resetSelectorData();
127 | this.sg.highlightNMostRecomputed(n);
128 | }
129 |
130 | resetSelectorData() {
131 | this.props.actions.uncheckSelector();
132 | }
133 |
134 | toggleDock() {
135 | this.setState({
136 | dockIsOpen: !this.state.dockIsOpen,
137 | });
138 | }
139 |
140 | handleCheckSelector(selectorToCheck) {
141 | this.props.actions.checkSelector(selectorToCheck);
142 | }
143 |
144 | renderGraph(graph) {
145 | const { checkedSelector } = this.props;
146 | const { dockIsOpen } = this.state;
147 |
148 | const dockMessage = (!checkedSelector || checkedSelector.isNamed) ?
149 | 'checkSelector output' : 'name selector to get data';
150 |
151 | return (
152 |
153 |
158 |
159 |
164 | { checkedSelector ?
165 | :
168 | No Data
169 | }
170 |
171 |
172 | this.sg = sg}
176 | {...graph}
177 | />
178 |
179 |
180 | );
181 | }
182 |
183 | renderContent() {
184 | const { graph } = this.props;
185 | if (graph.fetchedSuccessfully) return this.renderGraph(graph);
186 | if (graph.fetching) return renderMessage('Loading...');
187 | return renderMessage('Could not load selector graph');
188 | }
189 |
190 | render() {
191 | return (
192 |
193 |
198 | { this.renderContent() }
199 |
200 | );
201 | }
202 | }
203 |
204 |
--------------------------------------------------------------------------------
/extension/app/containers/Root.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Provider } from 'react-redux';
3 | import App from './App';
4 |
5 | export default class Root extends Component {
6 |
7 | static propTypes = {
8 | store: PropTypes.object.isRequired
9 | };
10 |
11 | render() {
12 | const { store } = this.props;
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/extension/app/reducers/graph.js:
--------------------------------------------------------------------------------
1 | import * as ActionTypes from '../constants/ActionTypes';
2 |
3 | const initialState = {
4 | fetching: false,
5 | fetchedSuccessfully: false,
6 | fetchedOnce: false,
7 | nodes: {},
8 | edges: [],
9 | checkedSelectorId: null,
10 | };
11 |
12 | const actionsMap = {
13 | [ActionTypes.GET_SELECTOR_GRAPH_FAILED](state) {
14 | return { ...state, fetching: false, fetchedSuccessfully: false };
15 | },
16 | [ActionTypes.GET_SELECTOR_GRAPH_SUCCESS](state, action) {
17 | const { nodes, edges } = action.payload.graph;
18 | const oldNodes = state.nodes;
19 | const mergedNodes = {};
20 | Object.keys(nodes).forEach((id) => {
21 | const node = { id, ...oldNodes[id], ...nodes[id] };
22 | if (node.isNamed === undefined) {
23 | node.isNamed = node.isRegistered;
24 | }
25 | mergedNodes[id] = node;
26 | });
27 |
28 | return {
29 | ...state,
30 | fetching: false,
31 | nodes: mergedNodes,
32 | edges,
33 | fetchedSuccessfully: true
34 | };
35 | },
36 | [ActionTypes.GET_SELECTOR_GRAPH](state) {
37 | return {
38 | ...state,
39 | fetchedOnce: true,
40 | fetching: true,
41 | };
42 | },
43 | [ActionTypes.UNCHECK_SELECTOR](state) {
44 | return { ...state, checkedSelectorId: null };
45 | },
46 | [ActionTypes.CHECK_SELECTOR_SUCCESS](state, action) {
47 | const { selector } = action.payload;
48 | const { nodes } = state;
49 | const { id } = selector;
50 | return {
51 | ...state,
52 | checkedSelectorId: id,
53 | nodes: {
54 | ...nodes,
55 | [id]: { ...nodes[id], ...selector }
56 | }
57 | };
58 | },
59 | [ActionTypes.CHECK_SELECTOR_FAILED](state, action) {
60 | // set it anyway
61 | return { ...state, checkedSelectorId: action.payload.selector.id };
62 | },
63 | };
64 |
65 | export default function graph(state = initialState, action) {
66 | const reduceFn = actionsMap[action.type];
67 | if (!reduceFn) return state;
68 | return reduceFn(state, action);
69 | }
70 |
--------------------------------------------------------------------------------
/extension/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import graph from './graph';
3 |
4 | export default combineReducers({
5 | graph
6 | });
7 |
--------------------------------------------------------------------------------
/extension/app/store/configureStore.dev.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from '../reducers';
4 | // import storage from '../utils/storage';
5 |
6 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose
7 | /* eslint-disable no-underscore-dangle */
8 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
9 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
10 | // Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html
11 | }) :
12 | compose;
13 | /* eslint-enable no-underscore-dangle */
14 |
15 |
16 | export default function (initialState, ...middlewares) {
17 | const enhancer = composeEnhancers(
18 | applyMiddleware(thunk, ...middlewares),
19 | // storage(),
20 | );
21 |
22 | const store = createStore(rootReducer, initialState, enhancer);
23 |
24 | if (module.hot) {
25 | module.hot.accept('../reducers', () => {
26 | const nextRootReducer = require('../reducers');
27 |
28 | store.replaceReducer(nextRootReducer);
29 | });
30 | }
31 | return store;
32 | }
33 |
--------------------------------------------------------------------------------
/extension/app/store/configureStore.js:
--------------------------------------------------------------------------------
1 | // Can't do dynamic imports with es6 import
2 | if (process.env.NODE_ENV === 'production') {
3 | module.exports = require('./configureStore.prod');
4 | } else {
5 | module.exports = require('./configureStore.dev');
6 | }
7 |
--------------------------------------------------------------------------------
/extension/app/store/configureStore.prod.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from '../reducers';
4 |
5 |
6 | export default function (initialState, ...middlewares) {
7 | const enhancer = compose(
8 | applyMiddleware(thunk, ...middlewares),
9 | );
10 |
11 | return createStore(rootReducer, initialState, enhancer);
12 | }
13 |
--------------------------------------------------------------------------------
/extension/app/utils/apiMiddleware.js:
--------------------------------------------------------------------------------
1 | import * as types from '../../app/constants/ActionTypes';
2 | import {
3 | checkSelectorSuccess,
4 | checkSelectorFailed,
5 | getSelectorGraphSuccess,
6 | getSelectorGraphFailed,
7 | } from '../../app/actions/graph';
8 |
9 | export default api => store => next => async (action) => {
10 | const result = next(action);
11 | if (action.type === types.CHECK_SELECTOR) {
12 | const { selector } = action.payload;
13 | const { id } = selector;
14 | try {
15 | const checked = await api.checkSelector(id);
16 | store.dispatch(checkSelectorSuccess({ ...checked, id }));
17 | } catch (e) {
18 | store.dispatch(checkSelectorFailed(selector));
19 | }
20 | return result;
21 | }
22 |
23 | if (action.type === types.GET_SELECTOR_GRAPH) {
24 | try {
25 | const graph = await api.selectorGraph();
26 | store.dispatch(getSelectorGraphSuccess(graph));
27 | } catch (e) {
28 | store.dispatch(getSelectorGraphFailed());
29 | }
30 | }
31 |
32 | return result;
33 | };
34 |
--------------------------------------------------------------------------------
/extension/app/utils/rpc.js:
--------------------------------------------------------------------------------
1 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2 | const where = sender.tab ? 'a content script' : 'the extension';
3 | const message = `extension received a message from ${where}`;
4 | console.log(message);
5 | sendResponse({ k: true });
6 | });
7 |
8 |
9 | function sendMessage(data) {
10 | console.log(chrome.windows.getCurrent((x) => console.log(x)));
11 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
12 | console.log('sending', data, tabs);
13 | chrome.tabs.sendMessage(tabs[0].id, data, function(response) {
14 | console.log(response);
15 | });
16 | });
17 | }
18 |
19 | export default (store) => (next) => (action) => {
20 | switch (action.type) {
21 | case 'ADD_TODO':
22 | sendMessage(action.text);
23 | default:
24 | break;
25 |
26 | }
27 | return next(action);
28 | };
29 |
30 |
31 |
--------------------------------------------------------------------------------
/extension/app/utils/storage.js:
--------------------------------------------------------------------------------
1 | function saveState(state) {
2 | chrome.storage.local.set({ state: JSON.stringify(state) });
3 | }
4 |
5 | // todos unmarked count
6 | function setBadge(todos) {
7 | if (chrome.browserAction) {
8 | const count = todos.filter(todo => !todo.marked).length;
9 | chrome.browserAction.setBadgeText({ text: count > 0 ? count.toString() : '' });
10 | }
11 | }
12 |
13 | export default function () {
14 | return next => (reducer, initialState) => {
15 | const store = next(reducer, initialState);
16 | store.subscribe(() => {
17 | const state = store.getState();
18 | saveState(state);
19 | setBadge(state.todos);
20 | });
21 | return store;
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/extension/appveyor.yml:
--------------------------------------------------------------------------------
1 | environment:
2 | matrix:
3 | - nodejs_version: '6'
4 | - nodejs_version: '7'
5 |
6 | cache:
7 | - "%LOCALAPPDATA%/Yarn"
8 | - node_modules
9 |
10 | install:
11 | - ps: Install-Product node $env:nodejs_version
12 | - yarn install
13 |
14 | test_script:
15 | - node --version
16 | - yarn --version
17 | - yarn run lint
18 | - yarn test
19 | - yarn run build
20 |
21 | build: off
22 |
--------------------------------------------------------------------------------
/extension/chrome/assets/img/icon-128-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/extension/chrome/assets/img/icon-128-disabled.png
--------------------------------------------------------------------------------
/extension/chrome/assets/img/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/extension/chrome/assets/img/icon-128.png
--------------------------------------------------------------------------------
/extension/chrome/assets/img/icon-16-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/extension/chrome/assets/img/icon-16-disabled.png
--------------------------------------------------------------------------------
/extension/chrome/assets/img/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/extension/chrome/assets/img/icon-16.png
--------------------------------------------------------------------------------
/extension/chrome/assets/img/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/extension/chrome/assets/img/icon-32.png
--------------------------------------------------------------------------------
/extension/chrome/assets/img/icon-48-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/extension/chrome/assets/img/icon-48-disabled.png
--------------------------------------------------------------------------------
/extension/chrome/assets/img/icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skortchmark9/reselect-tools/1175df8465356b94f39af2197019c44355c957f4/extension/chrome/assets/img/icon-48.png
--------------------------------------------------------------------------------
/extension/chrome/extension/background.js:
--------------------------------------------------------------------------------
1 | chrome.browserAction.onClicked.addListener(() => {
2 | window.open('https://github.com/skortchmark9/reselect-devtools-extension');
3 | });
4 |
--------------------------------------------------------------------------------
/extension/chrome/extension/devtools.js:
--------------------------------------------------------------------------------
1 | let panelCreated = false;
2 | let loadCheckInterval;
3 |
4 | const checkForDevtools = cb => chrome.devtools.inspectedWindow.eval('!!(Object.keys(window.__RESELECT_TOOLS__ || {}).length)', cb);
5 |
6 |
7 | function onCheck(pageHasDevtools) {
8 | if (!pageHasDevtools || panelCreated) {
9 | return;
10 | }
11 |
12 | clearInterval(loadCheckInterval);
13 | panelCreated = true;
14 | chrome.devtools.panels.create('Reselect', '', 'panel.html');
15 | }
16 |
17 | function createPanelIfDevtoolsLoaded() {
18 | if (panelCreated) return;
19 | checkForDevtools(onCheck);
20 | }
21 |
22 | chrome.devtools.network.onNavigated.addListener(createPanelIfDevtoolsLoaded);
23 |
24 | // Check to see if Reselect Tools have loaded once per second in case Reselect tools were added
25 | // after page load
26 | loadCheckInterval = setInterval(createPanelIfDevtoolsLoaded, 1000);
27 |
28 | createPanelIfDevtoolsLoaded();
29 |
--------------------------------------------------------------------------------
/extension/chrome/extension/page-api.js:
--------------------------------------------------------------------------------
1 | function evalPromise(str) {
2 | return new Promise((resolve, reject) => {
3 | chrome.devtools.inspectedWindow.eval(str, (resultStr, err) => {
4 | const result = JSON.parse(resultStr);
5 | if (err && err.isException) {
6 | console.error(err.value);
7 | reject(err.value);
8 | } else {
9 | resolve(result);
10 | }
11 | });
12 | });
13 | }
14 |
15 | export function checkSelector(id) {
16 | const str = `(function() {
17 | const __reselect_last_check = window.__RESELECT_TOOLS__.checkSelector('${id}');
18 | console.log(__reselect_last_check);
19 | return JSON.stringify(__reselect_last_check);
20 | })()`;
21 | return evalPromise(str);
22 | }
23 |
24 | export function selectorGraph() {
25 | const str = 'JSON.stringify(window.__RESELECT_TOOLS__.selectorGraph())';
26 | return evalPromise(str);
27 | }
28 |
--------------------------------------------------------------------------------
/extension/chrome/extension/panel.js:
--------------------------------------------------------------------------------
1 | import './reselect-tools-app';
2 |
3 |
--------------------------------------------------------------------------------
/extension/chrome/extension/reselect-tools-app.css:
--------------------------------------------------------------------------------
1 | html, body, body > div {
2 | height: 100%;
3 | width: 100%;
4 | }
5 | body {
6 | overflow: hidden;
7 | min-width: 350px;
8 | margin: 0;
9 | padding: 0;
10 | font-family: "Helvetica Neue", "Lucida Grande", sans-serif;
11 | font-size: 11px;
12 | background-color: rgb(0, 43, 54);
13 | color: #fff;
14 | }
15 | a {
16 | color: #fff;
17 | }
18 |
--------------------------------------------------------------------------------
/extension/chrome/extension/reselect-tools-app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Root from '../../app/containers/Root';
4 | import './reselect-tools-app.css';
5 |
6 | import * as api from './page-api';
7 |
8 | import createStore from '../../app/store/configureStore';
9 | import createApiMiddleware from '../../app/utils/apiMiddleware';
10 |
11 | const checkSelector = (id) => {
12 | if (id === 'c') {
13 | return Promise.resolve({ inputs: [1], output: {hey: 'hey'}, id, name: id });
14 | }
15 | if (id === 'b') {
16 | return Promise.resolve({ inputs: [1], output: 5, id, name: id });
17 | }
18 | if (id === 'a') {
19 | return Promise.resolve({ inputs: [5], output: 5, id, name: id });
20 | }
21 | return Promise.resolve({ inputs: [], output: 2, id, name: id });
22 | };
23 |
24 | const mockApi = {
25 | checkSelector,
26 | selectorGraph: () => {
27 | const a = { id: 'a', recomputations: 10, isNamed: true };
28 | const b = { id: 'b', recomputations: 10, isNamed: true };
29 | const c = { id: 'c', recomputations: 10, isNamed: true };
30 | const d = { id: 'd', recomputations: 2, isNamed: true };
31 | const e = { id: 'e', recomputations: 4, isNamed: true };
32 | const f = { id: 'f', recomputations: 6, isNamed: true };
33 | return Promise.resolve({ nodes: { a, b, c, d, e, f }, edges: [{ from: 'a', to: 'b' }, { from: 'b', to: 'c' }] });
34 | },
35 | };
36 |
37 |
38 | const apiMiddleware = createApiMiddleware(api);
39 | // const apiMiddleware = createApiMiddleware(window.location.origin === 'http://localhost:8000' ? mockApi : api);
40 |
41 |
42 | const initialState = {};
43 |
44 | ReactDOM.render(
45 | ,
46 | document.querySelector('#root')
47 | );
48 |
--------------------------------------------------------------------------------
/extension/chrome/manifest.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "background": {
3 | "page": "background.html"
4 | },
5 | "browser_action": {
6 | "default_icon": {
7 | "128": "img/icon-128.png",
8 | "16": "img/icon-16.png",
9 | "48": "img/icon-48.png"
10 | },
11 | "default_title": "Reselect Devtools"
12 | },
13 | "content_security_policy": "default-src 'self'; script-src 'self' http://localhost:3000 https://localhost:3000 'unsafe-eval'; connect-src http://localhost:3000 https://localhost:3000; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;",
14 | "description": "Reselect Devtools",
15 | "devtools_page": "devtools.html",
16 | "icons": {
17 | "128": "img/icon-128.png",
18 | "16": "img/icon-16.png",
19 | "48": "img/icon-48.png"
20 | },
21 | "manifest_version": 2,
22 | "name": "Reselect Devtools",
23 | "permissions": [
24 | "management",
25 | "tabs",
26 | "storage",
27 | ""
28 | ],
29 | "version": "0.0.2"
30 | }
--------------------------------------------------------------------------------
/extension/chrome/manifest.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "background": {
3 | "page": "background.html"
4 | },
5 | "browser_action": {
6 | "default_icon": {
7 | "128": "img/icon-128.png",
8 | "16": "img/icon-16.png",
9 | "32": "img/icon-32.png",
10 | "48": "img/icon-48.png"
11 | },
12 | "default_title": "Reselect Devtools"
13 | },
14 | "content_security_policy": "default-src 'self'; script-src 'self'; style-src * 'unsafe-inline'; img-src 'self' data:;",
15 | "description": "Reselect Devtools",
16 | "devtools_page": "devtools.html",
17 | "icons": {
18 | "128": "img/icon-128.png",
19 | "16": "img/icon-16.png",
20 | "32": "img/icon-32.png",
21 | "48": "img/icon-48.png"
22 | },
23 | "manifest_version": 2,
24 | "name": "Reselect Devtools",
25 | "permissions": [
26 | "tabs",
27 | "storage",
28 | ""
29 | ],
30 | "version": "0.0.2"
31 | }
--------------------------------------------------------------------------------
/extension/chrome/views/background.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 |
3 | html
4 | head
5 | script(src=env == 'prod' ? '/js/background.bundle.js' : 'http://localhost:3000/js/background.bundle.js')
6 |
--------------------------------------------------------------------------------
/extension/chrome/views/devtools.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 |
3 | html
4 | head
5 | meta(charset='UTF-8')
6 | title Reselect Devtools
7 | style.
8 | html, body {
9 | background-color: rgba(0,0,0,0)
10 | }
11 | body
12 | #root
13 | if env !== 'prod'
14 | script(src='chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/redux-devtools-extension.js')
15 | script(src=env == 'prod' ? '/js/devtools.bundle.js' : 'http://localhost:3000/js/devtools.bundle.js')
16 |
--------------------------------------------------------------------------------
/extension/chrome/views/panel.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 |
3 | html
4 | head
5 | meta(charset='UTF-8')
6 | title Redux TodoMVC Example (Panel)
7 | body
8 | #root
9 | if env !== 'prod'
10 | script(src='chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/redux-devtools-extension.js')
11 | script(src=env == 'prod' ? '/js/panel.bundle.js' : 'http://localhost:3000/js/panel.bundle.js')
12 |
--------------------------------------------------------------------------------
/extension/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/extension/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reselect-devtools-extension",
3 | "version": "0.0.1",
4 | "description": "Chrome Extension for Debugging Reselect",
5 | "scripts": {
6 | "dev": "node scripts/dev",
7 | "build": "node scripts/build",
8 | "compress": "node scripts/compress",
9 | "compress-keygen": "crx keygen",
10 | "clean": "rimraf build/ dev/ *.zip *.crx",
11 | "lint": "eslint app chrome test scripts webpack/*.js",
12 | "test-e2e": "cross-env NODE_ENV=test mocha -r ./test/setup-app test/e2e",
13 | "test": "cross-env NODE_ENV=test mocha -r ./test/setup-app test/app"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/skortchmark9/reselect-devtools-extension"
18 | },
19 | "keywords": [
20 | "chrome",
21 | "extension",
22 | "react",
23 | "redux",
24 | "reselect"
25 | ],
26 | "author": "Sam Kortchmar",
27 | "license": "MIT",
28 | "devDependencies": {
29 | "babel-core": "^6.3.15",
30 | "babel-eslint": "^7.1.0",
31 | "babel-loader": "^6.2.0",
32 | "babel-plugin-add-module-exports": "^0.2.1",
33 | "babel-plugin-transform-decorators-legacy": "^1.2.0",
34 | "babel-plugin-transform-runtime": "^6.5.2",
35 | "babel-preset-es2015": "^6.3.13",
36 | "babel-preset-react": "^6.3.13",
37 | "babel-preset-react-hmre": "^1.0.0",
38 | "babel-preset-react-optimize": "^1.0.1",
39 | "babel-preset-stage-0": "^6.3.13",
40 | "babel-runtime": "^6.3.19",
41 | "chai": "^3.2.0",
42 | "chromedriver": "^2.19.0",
43 | "cross-env": "^3.1.3",
44 | "crx": "^3.0.3",
45 | "css-loader": "^0.25.0",
46 | "css-modules-require-hook": "^4.0.5",
47 | "eslint": "^3.9.1",
48 | "eslint-config-airbnb": "^12.0.0",
49 | "eslint-plugin-import": "^1.16.0",
50 | "eslint-plugin-jsx-a11y": "^2.2.3",
51 | "eslint-plugin-react": "^6.5.0",
52 | "extract-text-webpack-plugin": "^1.0.1",
53 | "jsdom": "^9.2.1",
54 | "minimist": "^1.2.0",
55 | "mocha": "^3.1.2",
56 | "postcss-loader": "^1.2.1",
57 | "pug-cli": "^1.0.0-alpha6",
58 | "react-addons-test-utils": "^15.0.2",
59 | "rimraf": "^2.4.3",
60 | "selenium-webdriver": "^2.47.0",
61 | "shelljs": "^0.7.0",
62 | "sinon": "^1.17.1",
63 | "style-loader": "^0.13.1",
64 | "webpack": "^1.13.0",
65 | "webpack-hot-middleware": "^2.10.0",
66 | "webpack-httpolyglot-server": "^0.2.0"
67 | },
68 | "dependencies": {
69 | "bluebird": "^3.3.4",
70 | "classnames": "^2.1.3",
71 | "cytoscape": "^3.2.3",
72 | "cytoscape-dagre": "^2.1.0",
73 | "react": "^15.0.2",
74 | "react-autocomplete": "^1.7.2",
75 | "react-dom": "^15.0.2",
76 | "react-icons": "^2.2.7",
77 | "react-json-tree": "^0.11.0",
78 | "react-redux": "^4.3.0",
79 | "redux": "^3.2.1",
80 | "redux-devtools-inspector": "^0.11.3",
81 | "redux-thunk": "^2.0.1",
82 | "remotedev-app": "^0.10.8",
83 | "remotedev-monitor-components": "0.0.5"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/extension/scripts/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "shelljs": true
4 | },
5 | "rules": {
6 | "no-console": 0,
7 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/extension/scripts/build.js:
--------------------------------------------------------------------------------
1 | const tasks = require('./tasks');
2 |
3 | tasks.replaceWebpack();
4 | console.log('[Copy assets]');
5 | console.log('-'.repeat(80));
6 | tasks.copyAssets('build');
7 |
8 | console.log('[Webpack Build]');
9 | console.log('-'.repeat(80));
10 | exec('webpack --config webpack/prod.config.js --progress --profile --colors');
11 |
--------------------------------------------------------------------------------
/extension/scripts/compress.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const ChromeExtension = require('crx');
3 | /* eslint import/no-unresolved: 0 */
4 | const name = require('../build/manifest.json').name;
5 | const argv = require('minimist')(process.argv.slice(2));
6 |
7 | const keyPath = argv.key || 'key.pem';
8 | const existsKey = fs.existsSync(keyPath);
9 | const crx = new ChromeExtension({
10 | appId: argv['app-id'],
11 | codebase: argv.codebase,
12 | privateKey: existsKey ? fs.readFileSync(keyPath) : null
13 | });
14 |
15 | crx.load('build')
16 | .then(() => crx.loadContents())
17 | .then((archiveBuffer) => {
18 | fs.writeFile(`${name}.zip`, archiveBuffer);
19 |
20 | if (!argv.codebase || !existsKey) return;
21 | crx.pack(archiveBuffer).then((crxBuffer) => {
22 | const updateXML = crx.generateUpdateXML();
23 |
24 | fs.writeFile('update.xml', updateXML);
25 | fs.writeFile(`${name}.crx`, crxBuffer);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/extension/scripts/dev.js:
--------------------------------------------------------------------------------
1 | const tasks = require('./tasks');
2 | const createWebpackServer = require('webpack-httpolyglot-server');
3 | const devConfig = require('../webpack/dev.config');
4 |
5 | tasks.replaceWebpack();
6 | console.log('[Copy assets]');
7 | console.log('-'.repeat(80));
8 | tasks.copyAssets('dev');
9 |
10 | console.log('[Webpack Dev]');
11 | console.log('-'.repeat(80));
12 | console.log('If you\'re developing Inject page,');
13 | console.log('please allow `https://localhost:3000` connections in Google Chrome,');
14 | console.log('and load unpacked extensions with `./dev` folder. (see https://developer.chrome.com/extensions/getstarted#unpacked)\n');
15 | createWebpackServer(devConfig, {
16 | host: 'localhost',
17 | port: 3000
18 | });
19 |
--------------------------------------------------------------------------------
/extension/scripts/tasks.js:
--------------------------------------------------------------------------------
1 | require('shelljs/global');
2 |
3 | exports.replaceWebpack = () => {
4 | const replaceTasks = [{
5 | from: 'webpack/replace/JsonpMainTemplate.runtime.js',
6 | to: 'node_modules/webpack/lib/JsonpMainTemplate.runtime.js'
7 | }, {
8 | from: 'webpack/replace/process-update.js',
9 | to: 'node_modules/webpack-hot-middleware/process-update.js'
10 | }];
11 |
12 | replaceTasks.forEach(task => cp(task.from, task.to));
13 | };
14 |
15 | exports.copyAssets = (type) => {
16 | const env = type === 'build' ? 'prod' : type;
17 | rm('-rf', type);
18 | mkdir(type);
19 | cp(`chrome/manifest.${env}.json`, `${type}/manifest.json`);
20 | cp('-R', 'chrome/assets/*', type);
21 | exec(`pug -O "{ env: '${env}' }" -o ${type} chrome/views/`);
22 | };
23 |
--------------------------------------------------------------------------------
/extension/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "rules": {
6 | "no-unused-expressions": 0,
7 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/extension/test/app/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as types from '../../../app/constants/ActionTypes';
3 | import * as actions from '../../../app/actions/todos';
4 |
5 | describe('todoapp todo actions', () => {
6 | it('addTodo should create ADD_TODO action', () => {
7 | expect(actions.addTodo('Use Redux')).to.eql({
8 | type: types.ADD_TODO,
9 | text: 'Use Redux'
10 | });
11 | });
12 |
13 | it('deleteTodo should create DELETE_TODO action', () => {
14 | expect(actions.deleteTodo(1)).to.eql({
15 | type: types.DELETE_TODO,
16 | id: 1
17 | });
18 | });
19 |
20 | it('editTodo should create EDIT_TODO action', () => {
21 | expect(actions.editTodo(1, 'Use Redux everywhere')).to.eql({
22 | type: types.EDIT_TODO,
23 | id: 1,
24 | text: 'Use Redux everywhere'
25 | });
26 | });
27 |
28 | it('completeTodo should create COMPLETE_TODO action', () => {
29 | expect(actions.completeTodo(1)).to.eql({
30 | type: types.COMPLETE_TODO,
31 | id: 1
32 | });
33 | });
34 |
35 | it('completeAll should create COMPLETE_ALL action', () => {
36 | expect(actions.completeAll()).to.eql({
37 | type: types.COMPLETE_ALL
38 | });
39 | });
40 |
41 | it('clearCompleted should create CLEAR_COMPLETED action', () => {
42 | expect(actions.clearCompleted('Use Redux')).to.eql({
43 | type: types.CLEAR_COMPLETED
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/extension/test/app/components/Footer.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import sinon from 'sinon';
3 | import React from 'react';
4 | import TestUtils from 'react-addons-test-utils';
5 | import Footer from '../../../app/components/Footer';
6 | import style from '../../../app/components/Footer.css';
7 | import { SHOW_ALL, SHOW_ACTIVE } from '../../../app/constants/TodoFilters';
8 |
9 | function setup(propOverrides) {
10 | const props = {
11 | completedCount: 0,
12 | activeCount: 0,
13 | filter: SHOW_ALL,
14 | onClearCompleted: sinon.spy(),
15 | onShow: sinon.spy(),
16 | ...propOverrides
17 | };
18 |
19 | const renderer = TestUtils.createRenderer();
20 | renderer.render();
21 | const output = renderer.getRenderOutput();
22 |
23 | return { props, output };
24 | }
25 |
26 | function getTextContent(elem) {
27 | const children = Array.isArray(elem.props.children) ?
28 | elem.props.children : [elem.props.children];
29 |
30 | return children.reduce((out, child) =>
31 | // Children are either elements or text strings
32 | out + (child.props ? getTextContent(child) : child)
33 | , '');
34 | }
35 |
36 | describe('todoapp Footer component', () => {
37 | it('should render correctly', () => {
38 | const { output } = setup();
39 | expect(output.type).to.equal('footer');
40 | expect(output.props.className).to.equal(style.footer);
41 | });
42 |
43 | it('should display active count when 0', () => {
44 | const { output } = setup({ activeCount: 0 });
45 | const [count] = output.props.children;
46 | expect(getTextContent(count)).to.equal('No items left');
47 | });
48 |
49 | it('should display active count when above 0', () => {
50 | const { output } = setup({ activeCount: 1 });
51 | const [count] = output.props.children;
52 | expect(getTextContent(count)).to.equal('1 item left');
53 | });
54 |
55 | it('should render filters', () => {
56 | const { output } = setup();
57 | const [, filters] = output.props.children;
58 | expect(filters.type).to.equal('ul');
59 | expect(filters.props.className).to.equal(style.filters);
60 | expect(filters.props.children.length).to.equal(3);
61 | filters.props.children.forEach((filter, index) => {
62 | expect(filter.type).to.equal('li');
63 | const link = filter.props.children;
64 | expect(link.props.className).to.equal(index === 0 ? 'selected' : '');
65 | expect(link.props.children).to.equal(['All', 'Active', 'Completed'][index]);
66 | });
67 | });
68 |
69 | it('should call onShow when a filter is clicked', () => {
70 | const { output, props } = setup();
71 | const [, filters] = output.props.children;
72 | const filterLink = filters.props.children[1].props.children;
73 | filterLink.props.onClick({});
74 | expect(props.onShow.calledWith(SHOW_ACTIVE)).to.equal(true);
75 | });
76 |
77 | it('shouldnt show clear button when no completed todos', () => {
78 | const { output } = setup({ completedCount: 0 });
79 | const [,, clear] = output.props.children;
80 | expect(clear).to.equal(undefined);
81 | });
82 |
83 | it('should render clear button when completed todos', () => {
84 | const { output } = setup({ completedCount: 1 });
85 | const [,, clear] = output.props.children;
86 | expect(clear.type).to.equal('button');
87 | expect(clear.props.children).to.equal('Clear completed');
88 | });
89 |
90 | it('should call onClearCompleted on clear button click', () => {
91 | const { output, props } = setup({ completedCount: 1 });
92 | const [,, clear] = output.props.children;
93 | clear.props.onClick({});
94 | expect(props.onClearCompleted.called).to.equal(true);
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/extension/test/app/components/Header.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import sinon from 'sinon';
3 | import React from 'react';
4 | import TestUtils from 'react-addons-test-utils';
5 | import Header from '../../../app/components/Header';
6 | import TodoTextInput from '../../../app/components/TodoTextInput';
7 |
8 | function setup() {
9 | const props = {
10 | addTodo: sinon.spy()
11 | };
12 |
13 | const renderer = TestUtils.createRenderer();
14 | renderer.render();
15 | const output = renderer.getRenderOutput();
16 |
17 | return { props, output, renderer };
18 | }
19 |
20 | describe('todoapp Header component', () => {
21 | it('should render correctly', () => {
22 | const { output } = setup();
23 |
24 | expect(output.type).to.equal('header');
25 |
26 | const [h1, input] = output.props.children;
27 |
28 | expect(h1.type).to.equal('h1');
29 | expect(h1.props.children).to.equal('todos');
30 |
31 | expect(input.type).to.equal(TodoTextInput);
32 | expect(input.props.newTodo).to.equal(true);
33 | expect(input.props.placeholder).to.equal('What needs to be done?');
34 | });
35 |
36 | it('should call addTodo if length of text is greater than 0', () => {
37 | const { output, props } = setup();
38 | const input = output.props.children[1];
39 | input.props.onSave('');
40 | expect(props.addTodo.callCount).to.equal(0);
41 | input.props.onSave('Use Redux');
42 | expect(props.addTodo.callCount).to.equal(1);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/extension/test/app/components/MainSection.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import sinon from 'sinon';
3 | import React from 'react';
4 | import TestUtils from 'react-addons-test-utils';
5 | import MainSection from '../../../app/components/MainSection';
6 | import style from '../../../app/components/MainSection.css';
7 | import TodoItem from '../../../app/components/TodoItem';
8 | import Footer from '../../../app/components/Footer';
9 | import { SHOW_ALL, SHOW_COMPLETED } from '../../../app/constants/TodoFilters';
10 |
11 | function setup(propOverrides) {
12 | const props = {
13 | todos: [{
14 | text: 'Use Redux',
15 | completed: false,
16 | id: 0
17 | }, {
18 | text: 'Run the tests',
19 | completed: true,
20 | id: 1
21 | }],
22 | actions: {
23 | editTodo: sinon.spy(),
24 | deleteTodo: sinon.spy(),
25 | completeTodo: sinon.spy(),
26 | completeAll: sinon.spy(),
27 | clearCompleted: sinon.spy()
28 | },
29 | ...propOverrides
30 | };
31 |
32 | const renderer = TestUtils.createRenderer();
33 | renderer.render();
34 | const output = renderer.getRenderOutput();
35 |
36 | return { props, output, renderer };
37 | }
38 |
39 | describe('todoapp MainSection component', () => {
40 | it('should render correctly', () => {
41 | const { output } = setup();
42 | expect(output.type).to.equal('section');
43 | expect(output.props.className).to.equal(style.main);
44 | });
45 |
46 | describe('toggle all input', () => {
47 | it('should render', () => {
48 | const { output } = setup();
49 | const [toggle] = output.props.children;
50 | expect(toggle.type).to.equal('input');
51 | expect(toggle.props.type).to.equal('checkbox');
52 | expect(toggle.props.checked).to.equal(false);
53 | });
54 |
55 | it('should be checked if all todos completed', () => {
56 | const { output } = setup({
57 | todos: [{
58 | text: 'Use Redux',
59 | completed: true,
60 | id: 0
61 | }]
62 | });
63 | const [toggle] = output.props.children;
64 | expect(toggle.props.checked).to.equal(true);
65 | });
66 |
67 | it('should call completeAll on change', () => {
68 | const { output, props } = setup();
69 | const [toggle] = output.props.children;
70 | toggle.props.onChange({});
71 | expect(props.actions.completeAll.called).to.equal(true);
72 | });
73 | });
74 |
75 | describe('footer', () => {
76 | it('should render', () => {
77 | const { output } = setup();
78 | const [,, footer] = output.props.children;
79 | expect(footer.type).to.equal(Footer);
80 | expect(footer.props.completedCount).to.equal(1);
81 | expect(footer.props.activeCount).to.equal(1);
82 | expect(footer.props.filter).to.equal(SHOW_ALL);
83 | });
84 |
85 | it('onShow should set the filter', () => {
86 | const { output, renderer } = setup();
87 | const [,, footer] = output.props.children;
88 | footer.props.onShow(SHOW_COMPLETED);
89 | const updated = renderer.getRenderOutput();
90 | const [,, updatedFooter] = updated.props.children;
91 | expect(updatedFooter.props.filter).to.equal(SHOW_COMPLETED);
92 | });
93 |
94 | it('onClearCompleted should call clearCompleted', () => {
95 | const { output, props } = setup();
96 | const [,, footer] = output.props.children;
97 | footer.props.onClearCompleted();
98 | expect(props.actions.clearCompleted.called).to.equal(true);
99 | });
100 |
101 | it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
102 | const { output, props } = setup({
103 | todos: [{
104 | text: 'Use Redux',
105 | completed: false,
106 | id: 0
107 | }]
108 | });
109 | const [,, footer] = output.props.children;
110 | footer.props.onClearCompleted();
111 | expect(props.actions.clearCompleted.callCount).to.equal(0);
112 | });
113 | });
114 |
115 | describe('todo list', () => {
116 | it('should render', () => {
117 | const { output, props } = setup();
118 | const [, list] = output.props.children;
119 | expect(list.type).to.equal('ul');
120 | expect(list.props.children.length).to.equal(2);
121 | list.props.children.forEach((item, index) => {
122 | expect(item.type).to.equal(TodoItem);
123 | expect(item.props.todo).to.equal(props.todos[index]);
124 | });
125 | });
126 |
127 | it('should filter items', () => {
128 | const { output, renderer, props } = setup();
129 | const [,, footer] = output.props.children;
130 | footer.props.onShow(SHOW_COMPLETED);
131 | const updated = renderer.getRenderOutput();
132 | const [, updatedList] = updated.props.children;
133 | expect(updatedList.props.children.length).to.equal(1);
134 | expect(updatedList.props.children[0].props.todo).to.equal(props.todos[1]);
135 | });
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/extension/test/app/components/TodoItem.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import sinon from 'sinon';
3 | import React from 'react';
4 | import TestUtils from 'react-addons-test-utils';
5 | import TodoItem from '../../../app/components/TodoItem';
6 | import style from '../../../app/components/TodoItem.css';
7 | import TodoTextInput from '../../../app/components/TodoTextInput';
8 |
9 | function setup(editing = false) {
10 | const props = {
11 | todo: {
12 | id: 0,
13 | text: 'Use Redux',
14 | completed: false
15 | },
16 | editTodo: sinon.spy(),
17 | deleteTodo: sinon.spy(),
18 | completeTodo: sinon.spy()
19 | };
20 |
21 | const renderer = TestUtils.createRenderer();
22 |
23 | renderer.render();
24 |
25 | let output = renderer.getRenderOutput();
26 |
27 | if (editing) {
28 | const label = output.props.children.props.children[1];
29 | label.props.onDoubleClick({});
30 | output = renderer.getRenderOutput();
31 | }
32 |
33 | return { props, output, renderer };
34 | }
35 |
36 | describe('todoapp TodoItem component', () => {
37 | it('should render correctly', () => {
38 | const { output } = setup();
39 |
40 | expect(output.type).to.equal('li');
41 | expect(output.props.className).to.equal(style.normal);
42 |
43 | const div = output.props.children;
44 |
45 | expect(div.type).to.equal('div');
46 | expect(div.props.className).to.equal(style.view);
47 |
48 | const [input, label, button] = div.props.children;
49 |
50 | expect(input.type).to.equal('input');
51 | expect(input.props.checked).to.equal(false);
52 |
53 | expect(label.type).to.equal('label');
54 | expect(label.props.children).to.equal('Use Redux');
55 |
56 | expect(button.type).to.equal('button');
57 | expect(button.props.className).to.equal(style.destroy);
58 | });
59 |
60 | it('input onChange should call completeTodo', () => {
61 | const { output, props } = setup();
62 | const input = output.props.children.props.children[0];
63 | input.props.onChange({});
64 | expect(props.completeTodo.calledWith(0)).to.equal(true);
65 | });
66 |
67 | it('button onClick should call deleteTodo', () => {
68 | const { output, props } = setup();
69 | const button = output.props.children.props.children[2];
70 | button.props.onClick({});
71 | expect(props.deleteTodo.calledWith(0)).to.equal(true);
72 | });
73 |
74 | it('label onDoubleClick should put component in edit state', () => {
75 | const { output, renderer } = setup();
76 | const label = output.props.children.props.children[1];
77 | label.props.onDoubleClick({});
78 | const updated = renderer.getRenderOutput();
79 | expect(updated.type).to.equal('li');
80 | expect(updated.props.className).to.equal(style.editing);
81 | });
82 |
83 | it('edit state render', () => {
84 | const { output } = setup(true);
85 |
86 | expect(output.type).to.equal('li');
87 | expect(output.props.className).to.equal(style.editing);
88 |
89 | const input = output.props.children;
90 | expect(input.type).to.equal(TodoTextInput);
91 | expect(input.props.text).to.equal('Use Redux');
92 | expect(input.props.editing).to.equal(true);
93 | });
94 |
95 | it('TodoTextInput onSave should call editTodo', () => {
96 | const { output, props } = setup(true);
97 | output.props.children.props.onSave('Use Redux');
98 | expect(props.editTodo.calledWith(0, 'Use Redux')).to.equal(true);
99 | });
100 |
101 | it('TodoTextInput onSave should call deleteTodo if text is empty', () => {
102 | const { output, props } = setup(true);
103 | output.props.children.props.onSave('');
104 | expect(props.deleteTodo.calledWith(0)).to.equal(true);
105 | });
106 |
107 | it('TodoTextInput onSave should exit component from edit state', () => {
108 | const { output, renderer } = setup(true);
109 | output.props.children.props.onSave('Use Redux');
110 | const updated = renderer.getRenderOutput();
111 | expect(updated.type).to.equal('li');
112 | expect(updated.props.className).to.equal(style.normal);
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/extension/test/app/components/TodoTextInput.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import sinon from 'sinon';
3 | import React from 'react';
4 | import TestUtils from 'react-addons-test-utils';
5 | import TodoTextInput from '../../../app/components/TodoTextInput';
6 | import style from '../../../app/components/TodoTextInput.css';
7 |
8 | function setup(propOverrides) {
9 | const props = {
10 | onSave: sinon.spy(),
11 | text: 'Use Redux',
12 | placeholder: 'What needs to be done?',
13 | editing: false,
14 | newTodo: false,
15 | ...propOverrides
16 | };
17 |
18 | const renderer = TestUtils.createRenderer();
19 |
20 | renderer.render();
21 |
22 | let output = renderer.getRenderOutput();
23 |
24 | output = renderer.getRenderOutput();
25 |
26 | return { props, output, renderer };
27 | }
28 |
29 | describe('todoapp TodoTextInput component', () => {
30 | it('should render correctly', () => {
31 | const { output } = setup();
32 | expect(output.props.placeholder).to.equal('What needs to be done?');
33 | expect(output.props.value).to.equal('Use Redux');
34 | expect(output.props.className).to.equal('');
35 | });
36 |
37 | it('should render correctly when editing=true', () => {
38 | const { output } = setup({ editing: true });
39 | expect(output.props.className).to.equal(style.edit);
40 | });
41 |
42 | it('should render correctly when newTodo=true', () => {
43 | const { output } = setup({ newTodo: true });
44 | expect(output.props.className).to.equal(style.new);
45 | });
46 |
47 | it('should update value on change', () => {
48 | const { output, renderer } = setup();
49 | output.props.onChange({ target: { value: 'Use Radox' } });
50 | const updated = renderer.getRenderOutput();
51 | expect(updated.props.value).to.equal('Use Radox');
52 | });
53 |
54 | it('should call onSave on return key press', () => {
55 | const { output, props } = setup();
56 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
57 | expect(props.onSave.calledWith('Use Redux')).to.equal(true);
58 | });
59 |
60 | it('should reset state on return key press if newTodo', () => {
61 | const { output, renderer } = setup({ newTodo: true });
62 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
63 | const updated = renderer.getRenderOutput();
64 | expect(updated.props.value).to.equal('');
65 | });
66 |
67 | it('should call onSave on blur', () => {
68 | const { output, props } = setup();
69 | output.props.onBlur({ target: { value: 'Use Redux' } });
70 | expect(props.onSave.calledWith('Use Redux')).to.equal(true);
71 | });
72 |
73 | it('shouldnt call onSave on blur if newTodo', () => {
74 | const { output, props } = setup({ newTodo: true });
75 | output.props.onBlur({ target: { value: 'Use Redux' } });
76 | expect(props.onSave.callCount).to.equal(0);
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/extension/test/app/reducers/todos.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as types from '../../../app/constants/ActionTypes';
3 | import todos from '../../../app/reducers/todos';
4 |
5 | describe('todoapp todos reducer', () => {
6 | it('should handle initial state', () => {
7 | expect(
8 | todos(undefined, {})
9 | ).to.eql([{
10 | text: 'Use Redux',
11 | completed: false,
12 | id: 0
13 | }]);
14 | });
15 |
16 | it('should handle ADD_TODO', () => {
17 | expect(
18 | todos([], {
19 | type: types.ADD_TODO,
20 | text: 'Run the tests'
21 | })
22 | ).to.eql([{
23 | text: 'Run the tests',
24 | completed: false,
25 | id: 0
26 | }]);
27 |
28 | expect(
29 | todos([{
30 | text: 'Use Redux',
31 | completed: false,
32 | id: 0
33 | }], {
34 | type: types.ADD_TODO,
35 | text: 'Run the tests'
36 | })
37 | ).to.eql([{
38 | text: 'Run the tests',
39 | completed: false,
40 | id: 1
41 | }, {
42 | text: 'Use Redux',
43 | completed: false,
44 | id: 0
45 | }]);
46 |
47 | expect(
48 | todos([{
49 | text: 'Run the tests',
50 | completed: false,
51 | id: 1
52 | }, {
53 | text: 'Use Redux',
54 | completed: false,
55 | id: 0
56 | }], {
57 | type: types.ADD_TODO,
58 | text: 'Fix the tests'
59 | })
60 | ).to.eql([{
61 | text: 'Fix the tests',
62 | completed: false,
63 | id: 2
64 | }, {
65 | text: 'Run the tests',
66 | completed: false,
67 | id: 1
68 | }, {
69 | text: 'Use Redux',
70 | completed: false,
71 | id: 0
72 | }]);
73 | });
74 |
75 | it('should handle DELETE_TODO', () => {
76 | expect(
77 | todos([{
78 | text: 'Run the tests',
79 | completed: false,
80 | id: 1
81 | }, {
82 | text: 'Use Redux',
83 | completed: false,
84 | id: 0
85 | }], {
86 | type: types.DELETE_TODO,
87 | id: 1
88 | })
89 | ).to.eql([{
90 | text: 'Use Redux',
91 | completed: false,
92 | id: 0
93 | }]);
94 | });
95 |
96 | it('should handle EDIT_TODO', () => {
97 | expect(
98 | todos([{
99 | text: 'Run the tests',
100 | completed: false,
101 | id: 1
102 | }, {
103 | text: 'Use Redux',
104 | completed: false,
105 | id: 0
106 | }], {
107 | type: types.EDIT_TODO,
108 | text: 'Fix the tests',
109 | id: 1
110 | })
111 | ).to.eql([{
112 | text: 'Fix the tests',
113 | completed: false,
114 | id: 1
115 | }, {
116 | text: 'Use Redux',
117 | completed: false,
118 | id: 0
119 | }]);
120 | });
121 |
122 | it('should handle COMPLETE_TODO', () => {
123 | expect(
124 | todos([{
125 | text: 'Run the tests',
126 | completed: false,
127 | id: 1
128 | }, {
129 | text: 'Use Redux',
130 | completed: false,
131 | id: 0
132 | }], {
133 | type: types.COMPLETE_TODO,
134 | id: 1
135 | })
136 | ).to.eql([{
137 | text: 'Run the tests',
138 | completed: true,
139 | id: 1
140 | }, {
141 | text: 'Use Redux',
142 | completed: false,
143 | id: 0
144 | }]);
145 | });
146 |
147 | it('should handle COMPLETE_ALL', () => {
148 | expect(
149 | todos([{
150 | text: 'Run the tests',
151 | completed: true,
152 | id: 1
153 | }, {
154 | text: 'Use Redux',
155 | completed: false,
156 | id: 0
157 | }], {
158 | type: types.COMPLETE_ALL
159 | })
160 | ).to.eql([{
161 | text: 'Run the tests',
162 | completed: true,
163 | id: 1
164 | }, {
165 | text: 'Use Redux',
166 | completed: true,
167 | id: 0
168 | }]);
169 |
170 | // Unmark if all todos are currently completed
171 | expect(
172 | todos([{
173 | text: 'Run the tests',
174 | completed: true,
175 | id: 1
176 | }, {
177 | text: 'Use Redux',
178 | completed: true,
179 | id: 0
180 | }], {
181 | type: types.COMPLETE_ALL
182 | })
183 | ).to.eql([{
184 | text: 'Run the tests',
185 | completed: false,
186 | id: 1
187 | }, {
188 | text: 'Use Redux',
189 | completed: false,
190 | id: 0
191 | }]);
192 | });
193 |
194 | it('should handle CLEAR_COMPLETED', () => {
195 | expect(
196 | todos([{
197 | text: 'Run the tests',
198 | completed: true,
199 | id: 1
200 | }, {
201 | text: 'Use Redux',
202 | completed: false,
203 | id: 0
204 | }], {
205 | type: types.CLEAR_COMPLETED
206 | })
207 | ).to.eql([{
208 | text: 'Use Redux',
209 | completed: false,
210 | id: 0
211 | }]);
212 | });
213 |
214 | it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
215 | expect(
216 | [{
217 | type: types.COMPLETE_TODO,
218 | id: 0
219 | }, {
220 | type: types.CLEAR_COMPLETED
221 | }, {
222 | type: types.ADD_TODO,
223 | text: 'Write more tests'
224 | }].reduce(todos, [{
225 | id: 0,
226 | completed: false,
227 | text: 'Use Redux'
228 | }, {
229 | id: 1,
230 | completed: false,
231 | text: 'Write tests'
232 | }])
233 | ).to.eql([{
234 | text: 'Write more tests',
235 | completed: false,
236 | id: 2
237 | }, {
238 | text: 'Write tests',
239 | completed: false,
240 | id: 1
241 | }]);
242 | });
243 | });
244 |
--------------------------------------------------------------------------------
/extension/test/e2e/injectpage.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import webdriver from 'selenium-webdriver';
3 | import { expect } from 'chai';
4 | import { delay, startChromeDriver, buildWebDriver } from '../func';
5 |
6 | describe('inject page (in github.com)', function test() {
7 | let driver;
8 | this.timeout(15000);
9 |
10 | before(async () => {
11 | await startChromeDriver();
12 | const extPath = path.resolve('build');
13 | driver = buildWebDriver(extPath);
14 | await driver.get('https://github.com');
15 | });
16 |
17 | after(async () => driver.quit());
18 |
19 | it('should open Github', async () => {
20 | const title = await driver.getTitle();
21 | expect(title).to.equal('The world\'s leading software development platform · GitHub');
22 | });
23 |
24 | it('should render inject app', async () => {
25 | await driver.wait(
26 | () => driver.findElements(webdriver.By.className('inject-react-example'))
27 | .then(elems => elems.length > 0),
28 | 10000,
29 | 'Inject app not found'
30 | );
31 | });
32 |
33 | it('should find `Open TodoApp` button', async () => {
34 | await driver.wait(
35 | () => driver.findElements(webdriver.By.css('.inject-react-example button'))
36 | .then(elems => elems.length > 0),
37 | 10000,
38 | 'Inject app `Open TodoApp` button not found'
39 | );
40 | });
41 |
42 | it('should find iframe', async () => {
43 | driver.findElement(webdriver.By.css('.inject-react-example button')).click();
44 | await delay(1000);
45 | await driver.wait(
46 | () => driver.findElements(webdriver.By.css('.inject-react-example iframe'))
47 | .then(elems => elems.length > 0),
48 | 10000,
49 | 'Inject app iframe not found'
50 | );
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/extension/test/e2e/todoapp.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import webdriver from 'selenium-webdriver';
3 | import { expect } from 'chai';
4 | import { delay, startChromeDriver, buildWebDriver } from '../func';
5 | import footerStyle from '../../app/components/Footer.css';
6 | import mainSectionStyle from '../../app/components/MainSection.css';
7 | import todoItemStyle from '../../app/components/TodoItem.css';
8 | import todoTextInputStyle from '../../app/components/TodoTextInput.css';
9 | import manifest from '../../chrome/manifest.prod.json';
10 |
11 | const extensionName = manifest.name;
12 |
13 | const findList = driver =>
14 | driver.findElements(webdriver.By.css(`.${mainSectionStyle.todoList} > li`));
15 |
16 | const addTodo = async (driver, key) => {
17 | // add todo
18 | driver.findElement(webdriver.By.className(todoTextInputStyle.new))
19 | .sendKeys(key + webdriver.Key.RETURN);
20 | await delay(1000);
21 | const todos = await findList(driver);
22 | return { todo: todos[0], count: todos.length };
23 | };
24 |
25 | const editTodo = async (driver, index, key) => {
26 | let todos = await findList(driver);
27 | const label = todos[index].findElement(webdriver.By.tagName('label'));
28 | // dbl click to enable textarea
29 | await driver.actions().doubleClick(label).perform();
30 | await delay(500);
31 | // typing & enter
32 | driver.actions().sendKeys(key + webdriver.Key.RETURN).perform();
33 | await delay(1000);
34 |
35 | todos = await findList(driver);
36 | return { todo: todos[index], count: todos.length };
37 | };
38 |
39 | const completeTodo = async (driver, index) => {
40 | let todos = await findList(driver);
41 | todos[index].findElement(webdriver.By.className(todoItemStyle.toggle)).click();
42 | await delay(1000);
43 | todos = await findList(driver);
44 | return { todo: todos[index], count: todos.length };
45 | };
46 |
47 | const deleteTodo = async (driver, index) => {
48 | let todos = await findList(driver);
49 | driver.executeScript(
50 | `document.querySelectorAll('.${mainSectionStyle.todoList} > li')[${index}]
51 | .getElementsByClassName('${todoItemStyle.destroy}')[0].style.display = 'block'`
52 | );
53 | todos[index].findElement(webdriver.By.className(todoItemStyle.destroy)).click();
54 | await delay(1000);
55 | todos = await findList(driver);
56 | return { count: todos.length };
57 | };
58 |
59 | describe('window (popup) page', function test() {
60 | let driver;
61 | this.timeout(15000);
62 |
63 | before(async () => {
64 | await startChromeDriver();
65 | const extPath = path.resolve('build');
66 | driver = buildWebDriver(extPath);
67 | await driver.get('chrome://extensions-frame');
68 | const elems = await driver.findElements(webdriver.By.xpath(
69 | '//div[contains(@class, "extension-list-item-wrapper") and ' +
70 | `.//h2[contains(text(), "${extensionName}")]]`
71 | ));
72 | const extensionId = await elems[0].getAttribute('id');
73 | await driver.get(`chrome-extension://${extensionId}/window.html`);
74 | });
75 |
76 | after(async () => driver.quit());
77 |
78 | it('should open Redux TodoMVC Example', async () => {
79 | const title = await driver.getTitle();
80 | expect(title).to.equal('Redux TodoMVC Example (Window)');
81 | });
82 |
83 | it('should can add todo', async () => {
84 | const { todo, count } = await addTodo(driver, 'Add tests');
85 | expect(count).to.equal(2);
86 | const text = await todo.findElement(webdriver.By.tagName('label')).getText();
87 | expect(text).to.equal('Add tests');
88 | });
89 |
90 | it('should can edit todo', async () => {
91 | const { todo, count } = await editTodo(driver, 0, 'Ya ');
92 | expect(count).to.equal(2);
93 | const text = await todo.findElement(webdriver.By.tagName('label')).getText();
94 | expect(text).to.equal('Add testsYa');
95 | });
96 |
97 | it('should can complete todo', async () => {
98 | const { todo, count } = await completeTodo(driver, 0);
99 | expect(count).to.equal(2);
100 | const className = await todo.getAttribute('class');
101 | const { completed, normal } = todoItemStyle;
102 | expect(className).to.equal(`${completed} ${normal}`);
103 | });
104 |
105 | it('should can complete all todos', async () => {
106 | driver.findElement(webdriver.By.className(mainSectionStyle.toggleAll)).click();
107 | const todos = await findList(driver);
108 | const classNames = await Promise.all(todos.map(todo => todo.getAttribute('class')));
109 | const { completed, normal } = todoItemStyle;
110 | expect(classNames.every(name => name === `${completed} ${normal}`)).to.equal(true);
111 | });
112 |
113 | it('should can delete todo', async () => {
114 | const { count } = await deleteTodo(driver, 0);
115 | expect(count).to.equal(1);
116 | });
117 |
118 | it('should can clear completed todos if completed todos count > 0', async () => {
119 | // current todo count: 1
120 | await addTodo(driver, 'Add 1');
121 | const { count } = await addTodo(driver, 'Add 2');
122 | expect(count).to.equal(3);
123 |
124 | await completeTodo(driver, 0);
125 | driver.findElement(webdriver.By.className(footerStyle.clearCompleted)).click();
126 |
127 | const todos = await findList(driver);
128 | const classNames = await Promise.all(todos.map(todo => todo.getAttribute('class')));
129 | expect(classNames.every(name => name !== 'completed')).to.equal(true);
130 | });
131 |
132 | it('should cannot clear completed todos if completed todos count = 0', async () => {
133 | const todos = await driver.findElements(webdriver.By.className(footerStyle.clearCompleted));
134 | expect(todos.length).to.equal(0);
135 | });
136 |
137 | it('should can filter active todos', async () => {
138 | // current todo count: 2
139 | await addTodo(driver, 'Add 1');
140 | const { count } = await addTodo(driver, 'Add 2');
141 | expect(count).to.equal(3);
142 |
143 | await completeTodo(driver, 0);
144 | let todos = await driver.findElements(webdriver.By.css(`.${footerStyle.filters} > li`));
145 | todos[1].click();
146 | await delay(1000);
147 | todos = await findList(driver);
148 | expect(todos.length).to.equal(2);
149 | });
150 |
151 | it('should can filter completed todos', async () => {
152 | // current todo count: 2
153 | let todos = await driver.findElements(webdriver.By.css(`.${footerStyle.filters} > li`));
154 | todos[2].click();
155 | await delay(1000);
156 | todos = await findList(driver);
157 | expect(todos.length).to.equal(1);
158 | });
159 | });
160 |
--------------------------------------------------------------------------------
/extension/test/func.js:
--------------------------------------------------------------------------------
1 | import chromedriver from 'chromedriver';
2 | import webdriver from 'selenium-webdriver';
3 |
4 | export function delay(time) {
5 | return new Promise(resolve => setTimeout(resolve, time));
6 | }
7 |
8 | let crdvIsStarted = false;
9 | export function startChromeDriver() {
10 | if (crdvIsStarted) return Promise.resolve();
11 | chromedriver.start();
12 | process.on('exit', chromedriver.stop);
13 | crdvIsStarted = true;
14 | return delay(1000);
15 | }
16 |
17 | export function buildWebDriver(extPath) {
18 | return new webdriver.Builder()
19 | .usingServer('http://localhost:9515')
20 | .withCapabilities({
21 | chromeOptions: {
22 | args: [`load-extension=${extPath}`]
23 | }
24 | })
25 | .forBrowser('chrome')
26 | .build();
27 | }
28 |
--------------------------------------------------------------------------------
/extension/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --compilers js:babel-core/register
2 | --recursive
3 |
--------------------------------------------------------------------------------
/extension/test/setup-app.js:
--------------------------------------------------------------------------------
1 | import { jsdom } from 'jsdom';
2 | import hook from 'css-modules-require-hook';
3 | import postCSSConfig from '../webpack/postcss.config';
4 |
5 | global.document = jsdom('');
6 | global.window = document.defaultView;
7 | global.navigator = global.window.navigator;
8 |
9 | hook({
10 | generateScopedName: '[name]__[local]___[hash:base64:5]',
11 | prepend: postCSSConfig.plugins,
12 | });
13 |
--------------------------------------------------------------------------------
/extension/webpack/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/extension/webpack/customPublicPath.js:
--------------------------------------------------------------------------------
1 | /* global __webpack_public_path__ __HOST__ __PORT__ */
2 | /* eslint no-global-assign: 0 camelcase: 0 */
3 |
4 | if (process.env.NODE_ENV === 'production') {
5 | __webpack_public_path__ = chrome.extension.getURL('/js/');
6 | } else {
7 | // In development mode,
8 | // the iframe of injectpage cannot get correct path,
9 | // it need to get parent page protocol.
10 | const path = `//${__HOST__}:${__PORT__}/js/`;
11 | if (location.protocol === 'https:' || location.search.indexOf('protocol=https') !== -1) {
12 | __webpack_public_path__ = `https:${path}`;
13 | } else {
14 | __webpack_public_path__ = `http:${path}`;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/extension/webpack/dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const postCSSConfig = require('./postcss.config');
4 |
5 | const host = 'localhost';
6 | const port = 3000;
7 | const customPath = path.join(__dirname, './customPublicPath');
8 | const hotScript = 'webpack-hot-middleware/client?path=__webpack_hmr&dynamicPublicPath=true';
9 |
10 | const baseDevConfig = () => ({
11 | devtool: 'eval-cheap-module-source-map',
12 | entry: {
13 | 'reselect-tools-app': [customPath, hotScript, path.join(__dirname, '../chrome/extension/reselect-tools-app')],
14 | background: [customPath, hotScript, path.join(__dirname, '../chrome/extension/background')],
15 | panel: [customPath, hotScript, path.join(__dirname, '../chrome/extension/panel')],
16 | devtools: [customPath, hotScript, path.join(__dirname, '../chrome/extension/devtools')],
17 | },
18 | devMiddleware: {
19 | publicPath: `http://${host}:${port}/js`,
20 | stats: {
21 | colors: true
22 | },
23 | noInfo: true,
24 | headers: { 'Access-Control-Allow-Origin': '*' }
25 | },
26 | hotMiddleware: {
27 | path: '/js/__webpack_hmr'
28 | },
29 | output: {
30 | path: path.join(__dirname, '../dev/js'),
31 | filename: '[name].bundle.js',
32 | chunkFilename: '[id].chunk.js'
33 | },
34 | postcss() {
35 | return postCSSConfig;
36 | },
37 | plugins: [
38 | new webpack.HotModuleReplacementPlugin(),
39 | new webpack.NoErrorsPlugin(),
40 | new webpack.IgnorePlugin(/[^/]+\/[\S]+.prod$/),
41 | new webpack.DefinePlugin({
42 | __HOST__: `'${host}'`,
43 | __PORT__: port,
44 | 'process.env': {
45 | NODE_ENV: JSON.stringify('development')
46 | }
47 | })
48 | ],
49 | resolve: {
50 | extensions: ['', '.js']
51 | },
52 | module: {
53 | loaders: [{
54 | test: /\.js$/,
55 | loader: 'babel',
56 | exclude: /node_modules/,
57 | query: {
58 | presets: ['react-hmre']
59 | }
60 | }, {
61 | test: /\.css$/,
62 | loaders: [
63 | 'style',
64 | 'css?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
65 | 'postcss'
66 | ]
67 | }]
68 | }
69 | });
70 |
71 | const appConfig = baseDevConfig();
72 |
73 | module.exports = [
74 | appConfig
75 | ];
76 |
--------------------------------------------------------------------------------
/extension/webpack/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | ]
4 | };
5 |
--------------------------------------------------------------------------------
/extension/webpack/prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const postCSSConfig = require('./postcss.config');
4 |
5 | const customPath = path.join(__dirname, './customPublicPath');
6 |
7 | module.exports = {
8 | entry: {
9 | 'reselect-tools-app': [customPath, path.join(__dirname, '../chrome/extension/reselect-tools-app')],
10 | background: [customPath, path.join(__dirname, '../chrome/extension/background')],
11 | panel: [customPath, path.join(__dirname, '../chrome/extension/panel')],
12 | devtools: [customPath, path.join(__dirname, '../chrome/extension/devtools')],
13 | },
14 | output: {
15 | path: path.join(__dirname, '../build/js'),
16 | filename: '[name].bundle.js',
17 | chunkFilename: '[id].chunk.js'
18 | },
19 | postcss() {
20 | return postCSSConfig;
21 | },
22 | plugins: [
23 | new webpack.optimize.OccurenceOrderPlugin(),
24 | new webpack.IgnorePlugin(/[^/]+\/[\S]+.dev$/),
25 | new webpack.optimize.DedupePlugin(),
26 | new webpack.optimize.UglifyJsPlugin({
27 | comments: false,
28 | compressor: {
29 | warnings: false
30 | }
31 | }),
32 | new webpack.DefinePlugin({
33 | 'process.env': {
34 | NODE_ENV: JSON.stringify('production')
35 | }
36 | })
37 | ],
38 | resolve: {
39 | extensions: ['', '.js']
40 | },
41 | module: {
42 | loaders: [{
43 | test: /\.js$/,
44 | loader: 'babel',
45 | exclude: /node_modules/,
46 | query: {
47 | presets: ['react-optimize']
48 | }
49 | }, {
50 | test: /\.css$/,
51 | loaders: [
52 | 'style',
53 | 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
54 | 'postcss'
55 | ]
56 | }]
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/extension/webpack/replace/JsonpMainTemplate.runtime.js:
--------------------------------------------------------------------------------
1 | /*
2 | MIT License http://www.opensource.org/licenses/mit-license.php
3 | Author Tobias Koppers @sokra
4 | */
5 | /*globals hotAddUpdateChunk parentHotUpdateCallback document XMLHttpRequest $require$ $hotChunkFilename$ $hotMainFilename$ */
6 | module.exports = function() {
7 | function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars
8 | hotAddUpdateChunk(chunkId, moreModules);
9 | if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules);
10 | }
11 |
12 | var context = this;
13 | function evalCode(code, context) {
14 | return (function() { return eval(code); }).call(context);
15 | }
16 |
17 | context.hotDownloadUpdateChunk = function (chunkId) { // eslint-disable-line no-unused-vars
18 | var src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
19 | var request = new XMLHttpRequest();
20 |
21 | request.onload = function() {
22 | evalCode(this.responseText, context);
23 | };
24 | request.open("get", src, true);
25 | request.send();
26 | }
27 |
28 | function hotDownloadManifest(callback) { // eslint-disable-line no-unused-vars
29 | if(typeof XMLHttpRequest === "undefined")
30 | return callback(new Error("No browser support"));
31 | try {
32 | var request = new XMLHttpRequest();
33 | var requestPath = $require$.p + $hotMainFilename$;
34 | request.open("GET", requestPath, true);
35 | request.timeout = 10000;
36 | request.send(null);
37 | } catch(err) {
38 | return callback(err);
39 | }
40 | request.onreadystatechange = function() {
41 | if(request.readyState !== 4) return;
42 | if(request.status === 0) {
43 | // timeout
44 | callback(new Error("Manifest request to " + requestPath + " timed out."));
45 | } else if(request.status === 404) {
46 | // no update available
47 | callback();
48 | } else if(request.status !== 200 && request.status !== 304) {
49 | // other failure
50 | callback(new Error("Manifest request to " + requestPath + " failed."));
51 | } else {
52 | // success
53 | try {
54 | var update = JSON.parse(request.responseText);
55 | } catch(e) {
56 | callback(e);
57 | return;
58 | }
59 | callback(null, update);
60 | }
61 | };
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/extension/webpack/replace/process-update.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Based heavily on https://github.com/webpack/webpack/blob/
3 | * c0afdf9c6abc1dd70707c594e473802a566f7b6e/hot/only-dev-server.js
4 | * Original copyright Tobias Koppers @sokra (MIT license)
5 | */
6 |
7 | /* global window __webpack_hash__ */
8 |
9 | if (!module.hot) {
10 | throw new Error("[HMR] Hot Module Replacement is disabled.");
11 | }
12 |
13 | var hmrDocsUrl = "http://webpack.github.io/docs/hot-module-replacement-with-webpack.html"; // eslint-disable-line max-len
14 |
15 | var lastHash;
16 | var failureStatuses = { abort: 1, fail: 1 };
17 | var applyOptions = { ignoreUnaccepted: true };
18 |
19 | function upToDate(hash) {
20 | if (hash) lastHash = hash;
21 | return lastHash == __webpack_hash__;
22 | }
23 |
24 | module.exports = function(hash, moduleMap, options) {
25 | var reload = options.reload;
26 | if (!upToDate(hash) && module.hot.status() == "idle") {
27 | if (options.log) console.log("[HMR] Checking for updates on the server...");
28 | check();
29 | }
30 |
31 | function check() {
32 | var cb = function(err, updatedModules) {
33 | if (err) return handleError(err);
34 |
35 | if(!updatedModules) {
36 | if (options.warn) {
37 | console.warn("[HMR] Cannot find update (Full reload needed)");
38 | console.warn("[HMR] (Probably because of restarting the server)");
39 | }
40 | performReload();
41 | return null;
42 | }
43 |
44 | var applyCallback = function(applyErr, renewedModules) {
45 | if (applyErr) return handleError(applyErr);
46 |
47 | if (!upToDate()) check();
48 |
49 | logUpdates(updatedModules, renewedModules);
50 | };
51 |
52 | var applyResult = module.hot.apply(applyOptions, applyCallback);
53 | // webpack 2 promise
54 | if (applyResult && applyResult.then) {
55 | // HotModuleReplacement.runtime.js refers to the result as `outdatedModules`
56 | applyResult.then(function(outdatedModules) {
57 | applyCallback(null, outdatedModules);
58 | });
59 | applyResult.catch(applyCallback);
60 | }
61 |
62 | };
63 |
64 | var result = module.hot.check(false, cb);
65 | // webpack 2 promise
66 | if (result && result.then) {
67 | result.then(function(updatedModules) {
68 | cb(null, updatedModules);
69 | });
70 | result.catch(cb);
71 | }
72 | }
73 |
74 | function logUpdates(updatedModules, renewedModules) {
75 | var unacceptedModules = updatedModules.filter(function(moduleId) {
76 | return renewedModules && renewedModules.indexOf(moduleId) < 0;
77 | });
78 |
79 | if(unacceptedModules.length > 0) {
80 | if (options.warn) {
81 | console.warn(
82 | "[HMR] The following modules couldn't be hot updated: " +
83 | "(Full reload needed)\n" +
84 | "This is usually because the modules which have changed " +
85 | "(and their parents) do not know how to hot reload themselves. " +
86 | "See " + hmrDocsUrl + " for more details."
87 | );
88 | unacceptedModules.forEach(function(moduleId) {
89 | console.warn("[HMR] - " + moduleMap[moduleId]);
90 | });
91 | }
92 | /* replaced part start */
93 | if (chrome && chrome.runtime && chrome.runtime.reload) {
94 | console.warn("[HMR] extension reload");
95 | chrome.runtime.reload();
96 | return;
97 | }
98 | /* replaced part end */
99 | performReload();
100 | return;
101 | }
102 |
103 | if (options.log) {
104 | if(!renewedModules || renewedModules.length === 0) {
105 | console.log("[HMR] Nothing hot updated.");
106 | } else {
107 | console.log("[HMR] Updated modules:");
108 | renewedModules.forEach(function(moduleId) {
109 | console.log("[HMR] - " + moduleMap[moduleId]);
110 | });
111 | }
112 |
113 | if (upToDate()) {
114 | console.log("[HMR] App is up to date.");
115 | }
116 | }
117 | }
118 |
119 | function handleError(err) {
120 | if (module.hot.status() in failureStatuses) {
121 | if (options.warn) {
122 | console.warn("[HMR] Cannot check for update (Full reload needed)");
123 | console.warn("[HMR] " + err.stack || err.message);
124 | }
125 | performReload();
126 | return;
127 | }
128 | if (options.warn) {
129 | console.warn("[HMR] Update check failed: " + err.stack || err.message);
130 | }
131 | }
132 |
133 | function performReload() {
134 | if (reload) {
135 | if (options.warn) console.warn("[HMR] Reloading page");
136 | window.location.reload();
137 | }
138 | }
139 | };
140 |
--------------------------------------------------------------------------------
/extension/webpack/test.config.js:
--------------------------------------------------------------------------------
1 | // for babel-plugin-webpack-loaders
2 | const config = require('./prod.config');
3 |
4 | module.exports = {
5 | output: {
6 | libraryTarget: 'commonjs2'
7 | },
8 | module: {
9 | loaders: config.module.loaders.slice(1) // remove babel-loader
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.__esModule = true;
4 | exports.createSelectorWithDependencies = createSelectorWithDependencies;
5 | exports.registerSelectors = registerSelectors;
6 | exports.reset = reset;
7 | exports.checkSelector = checkSelector;
8 | exports.getStateWith = getStateWith;
9 | exports.selectorGraph = selectorGraph;
10 |
11 | var _reselect = require('reselect');
12 |
13 | var _getState = null;
14 | var _allSelectors = new Set();
15 |
16 | var _isFunction = function _isFunction(func) {
17 | return typeof func === 'function';
18 | };
19 |
20 | /*
21 | * This function is only exported for legacy purposes.
22 | * It will be removed in future versions.
23 | *
24 | */
25 | function createSelectorWithDependencies() {
26 | return _reselect.createSelector.apply(undefined, arguments);
27 | }
28 |
29 | var _isSelector = function _isSelector(selector) {
30 | return selector && selector.resultFunc || _isFunction(selector);
31 | };
32 |
33 | var _addSelector = function _addSelector(selector) {
34 | _allSelectors.add(selector);
35 |
36 | var dependencies = selector.dependencies || [];
37 | dependencies.forEach(_addSelector);
38 | };
39 |
40 | function registerSelectors(selectors) {
41 | Object.keys(selectors).forEach(function (name) {
42 | var selector = selectors[name];
43 | if (_isSelector(selector)) {
44 | selector.selectorName = name;
45 | _addSelector(selector);
46 | }
47 | });
48 | }
49 |
50 | function reset() {
51 | _getState = null;
52 | _allSelectors = new Set();
53 | }
54 |
55 | function checkSelector(selector) {
56 | if (typeof selector === 'string') {
57 | var _iteratorNormalCompletion = true;
58 | var _didIteratorError = false;
59 | var _iteratorError = undefined;
60 |
61 | try {
62 | for (var _iterator = _allSelectors[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
63 | var possibleSelector = _step.value;
64 |
65 | if (possibleSelector.selectorName === selector) {
66 | selector = possibleSelector;
67 | break;
68 | }
69 | }
70 | } catch (err) {
71 | _didIteratorError = true;
72 | _iteratorError = err;
73 | } finally {
74 | try {
75 | if (!_iteratorNormalCompletion && _iterator.return) {
76 | _iterator.return();
77 | }
78 | } finally {
79 | if (_didIteratorError) {
80 | throw _iteratorError;
81 | }
82 | }
83 | }
84 | }
85 |
86 | if (!_isFunction(selector)) {
87 | throw new Error('Selector ' + selector + ' is not a function...has it been registered?');
88 | }
89 |
90 | var _selector = selector,
91 | _selector$dependencie = _selector.dependencies,
92 | dependencies = _selector$dependencie === undefined ? [] : _selector$dependencie,
93 | _selector$selectorNam = _selector.selectorName,
94 | selectorName = _selector$selectorNam === undefined ? null : _selector$selectorNam;
95 |
96 | var isNamed = typeof selectorName === 'string';
97 | var recomputations = selector.recomputations ? selector.recomputations() : null;
98 |
99 | var ret = { dependencies: dependencies, recomputations: recomputations, isNamed: isNamed, selectorName: selectorName };
100 | if (_getState) {
101 | var extra = {};
102 | var state = _getState();
103 |
104 | try {
105 | extra.inputs = dependencies.map(function (parentSelector) {
106 | return parentSelector(state);
107 | });
108 | } catch (e) {
109 | extra.error = 'checkSelector: error getting inputs of selector ' + selectorName + '. The error was:\n' + e;
110 | }
111 |
112 | try {
113 | extra.output = selector(state);
114 | } catch (e) {
115 | extra.error = 'checkSelector: error getting output of selector ' + selectorName + '. The error was:\n' + e;
116 | }
117 |
118 | Object.assign(ret, extra);
119 | }
120 |
121 | return ret;
122 | }
123 |
124 | function getStateWith(stateGetter) {
125 | _getState = stateGetter;
126 | }
127 |
128 | function _sumString(str) {
129 | return Array.from(str.toString()).reduce(function (sum, char) {
130 | return char.charCodeAt(0) + sum;
131 | }, 0);
132 | }
133 |
134 | var defaultSelectorKey = function defaultSelectorKey(selector) {
135 | if (selector.selectorName) {
136 | return selector.selectorName;
137 | }
138 |
139 | if (selector.name) {
140 | // if it's a vanilla function, it will have a name.
141 | return selector.name;
142 | }
143 |
144 | return (selector.dependencies || []).reduce(function (base, dep) {
145 | return base + _sumString(dep);
146 | }, (selector.resultFunc ? selector.resultFunc : selector).toString());
147 | };
148 |
149 | function selectorGraph() {
150 | var selectorKey = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultSelectorKey;
151 |
152 | var graph = { nodes: {}, edges: [] };
153 | var addToGraph = function addToGraph(selector) {
154 | var name = selectorKey(selector);
155 | if (graph.nodes[name]) return;
156 |
157 | var _checkSelector = checkSelector(selector),
158 | recomputations = _checkSelector.recomputations,
159 | isNamed = _checkSelector.isNamed;
160 |
161 | graph.nodes[name] = {
162 | recomputations: recomputations,
163 | isNamed: isNamed,
164 | name: name
165 | };
166 |
167 | var dependencies = selector.dependencies || [];
168 | dependencies.forEach(function (dependency) {
169 | addToGraph(dependency);
170 | graph.edges.push({ from: name, to: selectorKey(dependency) });
171 | });
172 | };
173 |
174 | var _iteratorNormalCompletion2 = true;
175 | var _didIteratorError2 = false;
176 | var _iteratorError2 = undefined;
177 |
178 | try {
179 | for (var _iterator2 = _allSelectors[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
180 | var selector = _step2.value;
181 |
182 | addToGraph(selector);
183 | }
184 | } catch (err) {
185 | _didIteratorError2 = true;
186 | _iteratorError2 = err;
187 | } finally {
188 | try {
189 | if (!_iteratorNormalCompletion2 && _iterator2.return) {
190 | _iterator2.return();
191 | }
192 | } finally {
193 | if (_didIteratorError2) {
194 | throw _iteratorError2;
195 | }
196 | }
197 | }
198 |
199 | return graph;
200 | }
201 |
202 | // hack for devtools
203 | /* istanbul ignore if */
204 | if (typeof window !== 'undefined') {
205 | window.__RESELECT_TOOLS__ = {
206 | selectorGraph: selectorGraph,
207 | checkSelector: checkSelector
208 | };
209 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reselect-tools",
3 | "version": "0.0.7",
4 | "description": "Selector Debugging Tools for Reselect.",
5 | "main": "lib/index.js",
6 | "jsnext:main": "es/index.js",
7 | "files": [
8 | "lib",
9 | "src",
10 | "dist",
11 | "es"
12 | ],
13 | "bugs": {
14 | "url": "https://github.com/skortchmark9/reselect-tools"
15 | },
16 | "scripts": {
17 | "compile:commonjs": "better-npm-run compile:commonjs",
18 | "compile:umdmin": "uglifyjs dist/reselect-tools.js -mt -o dist/reselect-tools.min.js",
19 | "compile:umd": "better-npm-run compile:umd",
20 | "compile:es": "babel -d es/ src/",
21 | "compile": "npm run compile:commonjs && npm run compile:umd && npm run compile:umdmin && npm run compile:es",
22 | "lint": "eslint src test",
23 | "lint-fix": "eslint --fix src test",
24 | "prepublish": "npm run compile",
25 | "test": "better-npm-run test",
26 | "test:cov": "better-npm-run test:cov",
27 | "example": "opn http://localhost:9080/examples/demo.html && static-server"
28 | },
29 | "betterScripts": {
30 | "test": {
31 | "command": "mocha --compilers js:babel-register --ui tdd --recursive",
32 | "env": {
33 | "NODE_ENV": "test"
34 | }
35 | },
36 | "test:cov": {
37 | "command": "nyc --reporter=lcov --reporter=text mocha --compilers js:babel-register --ui tdd",
38 | "env": {
39 | "NODE_ENV": "test",
40 | "COVERAGE": "true"
41 | }
42 | },
43 | "compile:commonjs": {
44 | "command": "babel -d lib/ src/",
45 | "env": {
46 | "NODE_ENV": "commonjs"
47 | }
48 | },
49 | "compile:umd": {
50 | "command": "mkdirp dist/ && babel -o dist/reselect-tools.js src/",
51 | "env": {
52 | "NODE_ENV": "umd"
53 | }
54 | }
55 | },
56 | "keywords": [
57 | "redux",
58 | "reselect"
59 | ],
60 | "authors": [
61 | "Sam Kortchmar"
62 | ],
63 | "repository": {
64 | "type": "git",
65 | "url": "https://github.com/skortchmark9/reselect-tools"
66 | },
67 | "license": "MIT",
68 | "devDependencies": {
69 | "babel-cli": "^6.7.5",
70 | "babel-plugin-transform-es2015-modules-commonjs": "^6.7.4",
71 | "babel-plugin-transform-es2015-modules-umd": "^6.6.5",
72 | "babel-preset-env": "^1.6.1",
73 | "babel-register": "^6.7.2",
74 | "better-npm-run": "0.0.8",
75 | "chai": "^3.0.0",
76 | "codecov.io": "^0.1.6",
77 | "coveralls": "^2.11.4",
78 | "eslint": "^2.11",
79 | "mkdirp": "^0.5.1",
80 | "mocha": "^2.2.5",
81 | "ncp": "^2.0.0",
82 | "nyc": "^6.4.0",
83 | "opn-cli": "^3.1.0",
84 | "uglify-js": "^3.0.20"
85 | },
86 | "dependencies": {
87 | "reselect": "4.0.0"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect'
2 |
3 | let _getState = null
4 | let _allSelectors = new Set()
5 |
6 |
7 | const _isFunction = (func) => typeof func === 'function'
8 |
9 | /*
10 | * This function is only exported for legacy purposes.
11 | * It will be removed in future versions.
12 | *
13 | */
14 | export function createSelectorWithDependencies(...args) {
15 | return createSelector(...args)
16 | }
17 |
18 | const _isSelector = (selector) => (selector && selector.resultFunc) || _isFunction(selector)
19 |
20 | const _addSelector = (selector) => {
21 | _allSelectors.add(selector)
22 |
23 | const dependencies = selector.dependencies || []
24 | dependencies.forEach(_addSelector)
25 | }
26 |
27 | export function registerSelectors(selectors) {
28 | Object.keys(selectors).forEach((name) => {
29 | const selector = selectors[name]
30 | if (_isSelector(selector)) {
31 | selector.selectorName = name
32 | _addSelector(selector)
33 | }
34 | })
35 | }
36 |
37 |
38 | export function reset() {
39 | _getState = null
40 | _allSelectors = new Set()
41 | }
42 |
43 |
44 | export function checkSelector(selector) {
45 | if (typeof selector === 'string') {
46 | for (const possibleSelector of _allSelectors) {
47 | if (possibleSelector.selectorName === selector) {
48 | selector = possibleSelector
49 | break
50 | }
51 | }
52 | }
53 |
54 | if (!_isFunction(selector)) {
55 | throw new Error(`Selector ${selector} is not a function...has it been registered?`)
56 | }
57 |
58 |
59 | const { dependencies = [], selectorName = null } = selector
60 |
61 | const isNamed = typeof selectorName === 'string'
62 | const recomputations = selector.recomputations ? selector.recomputations() : null
63 |
64 | const ret = { dependencies, recomputations, isNamed, selectorName }
65 | if (_getState) {
66 | const extra = {}
67 | const state = _getState()
68 |
69 | try {
70 | extra.inputs = dependencies.map((parentSelector) => parentSelector(state))
71 |
72 | try {
73 | extra.output = selector(state)
74 | } catch (e) {
75 | extra.error = `checkSelector: error getting output of selector ${selectorName}. The error was:\n${e}`
76 | }
77 | } catch (e) {
78 | extra.error = `checkSelector: error getting inputs of selector ${selectorName}. The error was:\n${e}`
79 | }
80 |
81 | Object.assign(ret, extra)
82 | }
83 |
84 | return ret
85 | }
86 |
87 |
88 | export function getStateWith(stateGetter) {
89 | _getState = stateGetter
90 | }
91 |
92 |
93 | function _sumString(str) {
94 | return Array.from(str.toString()).reduce((sum, char) => char.charCodeAt(0) + sum, 0)
95 | }
96 |
97 | const defaultSelectorKey = (selector) => {
98 | if (selector.selectorName) {
99 | return selector.selectorName
100 | }
101 |
102 | if (selector.name) { // if it's a vanilla function, it will have a name.
103 | return selector.name
104 | }
105 |
106 | return (selector.dependencies || []).reduce((base, dep) => {
107 | return base + _sumString(dep)
108 | }, (selector.resultFunc ? selector.resultFunc : selector).toString())
109 | }
110 |
111 | export function selectorGraph(selectorKey = defaultSelectorKey) {
112 | const graph = { nodes: {}, edges: [] }
113 | const addToGraph = (selector) => {
114 | const name = selectorKey(selector)
115 | if (graph.nodes[name]) return
116 | const { recomputations, isNamed } = checkSelector(selector)
117 | graph.nodes[name] = {
118 | recomputations,
119 | isNamed,
120 | name
121 | }
122 |
123 | let dependencies = selector.dependencies || []
124 | dependencies.forEach((dependency) => {
125 | addToGraph(dependency)
126 | graph.edges.push({ from: name, to: selectorKey(dependency) })
127 | })
128 | }
129 |
130 | for (let selector of _allSelectors) {
131 | addToGraph(selector)
132 | }
133 | return graph
134 | }
135 |
136 | // hack for devtools
137 | /* istanbul ignore if */
138 | if (typeof window !== 'undefined') {
139 | window.__RESELECT_TOOLS__ = {
140 | selectorGraph,
141 | checkSelector
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | import chai from 'chai'
2 | import { createSelector } from 'reselect'
3 | import {
4 | registerSelectors,
5 | createSelectorWithDependencies,
6 | getStateWith,
7 | checkSelector,
8 | selectorGraph,
9 | reset } from '../src/index'
10 |
11 | const assert = chai.assert
12 |
13 | beforeEach(reset)
14 |
15 | suite('registerSelectors', () => {
16 |
17 | test('allows you to name selectors', () => {
18 | const foo = () => 'foo'
19 | const bar = createSelector(foo, () => 'bar')
20 | const baz = createSelector(bar, foo, () => 'baz')
21 | registerSelectors({ foo, bar, bazinga: baz })
22 |
23 | assert.equal(foo.selectorName, 'foo')
24 | assert.equal(bar.selectorName, 'bar')
25 | assert.equal(baz.selectorName, 'bazinga')
26 | })
27 |
28 | test('ignores inputs which are not selectors or functions', () => {
29 | const foo = () => 'foo'
30 | const bar = createSelector(foo, () => 'bar')
31 | const utilities = {
32 | identity: x => x
33 | }
34 | const selectors = { foo, bar, utilities }
35 | registerSelectors(selectors)
36 |
37 | assert.isUndefined(utilities.selectorName)
38 | })
39 |
40 | test('ignores inputs which are null', () => {
41 | const foo = () => 'foo'
42 | const bar = createSelector(foo, () => 'bar')
43 | const selectors = { foo, bar, property: null }
44 | registerSelectors(selectors)
45 | })
46 |
47 | test('can be called additively', () => {
48 | const foo = () => 'foo'
49 | const bar = createSelector(foo, () => 'bar')
50 | const baz = createSelector(bar, foo, () => 'bar')
51 |
52 | registerSelectors({ foo, bar })
53 | assert.equal(foo.selectorName, 'foo')
54 |
55 | registerSelectors({ baz })
56 | registerSelectors({ hat: foo })
57 | assert.equal(foo.selectorName, 'hat')
58 | assert.equal(bar.selectorName, 'bar')
59 | assert.equal(baz.selectorName, 'baz')
60 | })
61 | })
62 |
63 | suite('createSelectorWithDependencies', () => {
64 | test('it is just exported for legacy purposes', () => {
65 | const four = () => 4
66 | let calls1 = 0
67 | let calls2 = 0
68 | const selector1 = createSelector(four, () => calls1++)
69 | const selector2 = createSelectorWithDependencies(four, () => calls2++)
70 |
71 | selector1()
72 | selector1()
73 | selector2()
74 | selector2()
75 | assert.equal(calls1, calls2)
76 | })
77 | })
78 |
79 | suite('checkSelector', () => {
80 |
81 | test('it outputs a selector\'s dependencies, even if it\'s a plain function', () => {
82 | const foo = () => 'foo'
83 | const bar = createSelector(foo, () => 'bar')
84 |
85 | assert.equal(checkSelector(foo).dependencies.length, 0)
86 |
87 | assert.equal(checkSelector(bar).dependencies.length, 1)
88 | assert.equal(checkSelector(bar).dependencies[0], foo)
89 | })
90 |
91 | test('if you give it a way of getting state, it also gets inputs and outputs', () => {
92 | const state = {
93 | foo: {
94 | baz: 1
95 | }
96 | }
97 |
98 | getStateWith(() => state)
99 |
100 | const foo = (state) => state.foo
101 | const bar = createSelector(foo, (foo) => foo.baz)
102 |
103 | const checkedFoo = checkSelector(foo)
104 | assert.equal(checkedFoo.inputs.length, 0)
105 | assert.deepEqual(checkedFoo.output, { baz: 1 })
106 | assert.deepEqual(checkedFoo.output, foo(state))
107 |
108 | const checkedBar = checkSelector(bar)
109 | assert.deepEqual(checkedBar.inputs, [ { baz: 1 } ])
110 | assert.equal(checkedBar.output, 1)
111 | assert.deepEqual(checkedBar.output, bar(state))
112 |
113 | getStateWith(null)
114 | })
115 |
116 | test('it returns the number of recomputations for a given selector', () => {
117 | const foo = (state) => state.foo
118 | const bar = createSelector(foo, (foo) => foo.baz)
119 | assert.equal(bar.recomputations(), 0)
120 |
121 | const state = {
122 | foo: {
123 | baz: 1
124 | }
125 | }
126 | getStateWith(() => state)
127 |
128 | bar(state)
129 | assert.equal(bar.recomputations(), 1)
130 | bar(state)
131 |
132 | assert.deepEqual(checkSelector(bar), {
133 | dependencies: [ foo ],
134 | inputs: [ { baz : 1 } ],
135 | output: 1,
136 | recomputations: 1,
137 | isNamed: false,
138 | selectorName: null
139 | })
140 |
141 | const newState = {
142 | foo: {
143 | baz: 2
144 | }
145 | }
146 | getStateWith(() => newState)
147 |
148 | bar(newState)
149 | assert.equal(bar.recomputations(), 2)
150 |
151 | bar(newState)
152 | assert.deepEqual(checkSelector(bar), {
153 | dependencies: [ foo ],
154 | inputs: [ { baz : 2 } ],
155 | output: 2,
156 | recomputations: 2,
157 | isNamed: false,
158 | selectorName: null
159 | })
160 | })
161 |
162 | test("it allows you to pass in a string name of a selector if you've registered", () => {
163 | const foo = (state) => state.foo
164 | const bar = createSelector(foo, (foo) => foo + 1)
165 | registerSelectors({ bar })
166 | getStateWith(() => ({ foo: 1 }))
167 | const checked = checkSelector('bar')
168 | assert.deepEqual(checked, {
169 | dependencies: [ foo ],
170 | inputs: [ 1 ],
171 | output: 2,
172 | recomputations: 0,
173 | isNamed: true,
174 | selectorName: 'bar'
175 | })
176 | })
177 |
178 | test('it throws if you try to check a non-existent selector', () => {
179 | const foo = (state) => state.foo
180 | const bar = createSelector(foo, (foo) => foo + 1)
181 | registerSelectors({ bar })
182 | assert.throws(() => checkSelector('baz'))
183 | })
184 |
185 | test('it throws if you try to check a non-function', () => {
186 | assert.throws(() => checkSelector(1))
187 | })
188 |
189 | test('it tells you whether or not a selector has been registered', () => {
190 | const one$ = () => 1
191 | const two$ = createSelector(one$, (one) => one + 1)
192 | registerSelectors({ one$ })
193 |
194 | assert.equal(checkSelector(() => 1).isNamed, false)
195 |
196 | assert.equal(checkSelector(two$).isNamed, false)
197 | registerSelectors({ two$ })
198 | assert.equal(checkSelector(two$).isNamed, true)
199 | })
200 |
201 | test('it catches errors inside parent selector functions and exposes them', () => {
202 | const badParentSelector$ = (state) => state.foo.bar
203 | const badSelector$ = createSelector(badParentSelector$, (foo) => foo)
204 | getStateWith(() => [])
205 | registerSelectors({ badSelector$ })
206 |
207 | const checked = checkSelector('badSelector$')
208 | assert.equal(checked.error, 'checkSelector: error getting inputs of selector badSelector$. The error was:\n' +
209 | 'TypeError: Cannot read property \'bar\' of undefined')
210 | })
211 |
212 | test('it catches errors inside selector functions and exposes them', () => {
213 | const badSelector$ = (state) => state.foo.bar
214 | getStateWith(() => [])
215 | registerSelectors({ badSelector$ })
216 |
217 | const checked = checkSelector('badSelector$')
218 | assert.equal(checked.error, 'checkSelector: error getting output of selector badSelector$. The error was:\n' +
219 | 'TypeError: Cannot read property \'bar\' of undefined')
220 | })
221 | })
222 |
223 | suite('selectorGraph', () => {
224 | function createMockSelectors() {
225 | const data$ = (state) => state.data
226 | const ui$ = (state) => state.ui
227 | const users$ = createSelector(data$, (data) => data.users)
228 | const pets$ = createSelector(data$, ({ pets }) => pets)
229 | const currentUser$ = createSelector(ui$, users$, (ui, users) => users[ui.currentUser])
230 | const currentUserPets$ = createSelector(currentUser$, pets$, (currentUser, pets) => currentUser.pets.map((petId) => pets[petId]))
231 | const random$ = () => 1
232 | const thingy$ = createSelector(random$, (number) => number + 1)
233 | const booya$ = createSelector(thingy$, currentUser$, () => 'booya!')
234 | const selectors = {
235 | data$,
236 | ui$,
237 | users$,
238 | pets$,
239 | currentUser$,
240 | currentUserPets$,
241 | random$,
242 | thingy$,
243 | booya$
244 | }
245 | registerSelectors(selectors)
246 | return selectors
247 | }
248 |
249 | test('returns an empty graph if no selectors are registered', () => {
250 | const { edges, nodes } = selectorGraph()
251 | assert.equal(Object.keys(nodes).length, 0)
252 | assert.equal(edges.length, 0)
253 | })
254 |
255 | test('walks up the tree if you register a single selector', () => {
256 | function parent() { return 'parent' }
257 | const child$ = createSelector(parent, (string) => string)
258 | registerSelectors({ child$ })
259 | const { edges, nodes } = selectorGraph()
260 | assert.equal(Object.keys(nodes).length, 2)
261 | assert.equal(edges.length, 1)
262 | })
263 |
264 | test('it outputs a selector graph', () => {
265 | const selectors = createMockSelectors()
266 |
267 | const { edges, nodes } = selectorGraph()
268 | assert.equal(Object.keys(nodes).length, Object.keys(selectors).length)
269 | assert.equal(edges.length, 9)
270 | })
271 |
272 | test('allows you to pass in a different selector key function', () => {
273 | function idxSelectorKey(selector) {
274 | return selector.idx
275 | }
276 |
277 | const selectors = createMockSelectors()
278 | Object.keys(selectors).sort().forEach((key, i) => {
279 | const selector = selectors[key]
280 | selector.idx = i
281 | })
282 |
283 | const { nodes } = selectorGraph(idxSelectorKey)
284 | assert.equal(Object.keys(nodes).length, 9)
285 | })
286 |
287 | suite('defaultSelectorKey', () => {
288 | test('it names the nodes based on their string name by default', () => {
289 | createMockSelectors()
290 | const { nodes } = selectorGraph()
291 |
292 | // comes from func.name for top-level vanilla selector functions.
293 | assert.equal(nodes['data$'].recomputations, null)
294 | })
295 |
296 | test('it falls back to toString on anonymous functions', () => {
297 | const selector1 = createSelector(() => 1, (one) => one + 1)
298 | registerSelectors({ selector1 })
299 | const { nodes } = selectorGraph()
300 | const keys = Object.keys(nodes)
301 | assert.equal(keys.length, 2)
302 |
303 | // [ 'selector1', 'function () {\n return 1;\n }' ]
304 | for (let key of keys) {
305 | assert.include(key, '1')
306 | }
307 | })
308 |
309 | test('it creates numeric names for unregistered selectors', () => {
310 | const foo$ = createSelector(() => 'foo')
311 | const unregistered$ = createSelector(foo$, () => 1)
312 | const registered$ = createSelector(unregistered$, () => 3)
313 |
314 | registerSelectors({ registered$, foo$ })
315 | const { nodes } = selectorGraph()
316 | const keys = Object.keys(nodes)
317 | assert.equal(keys.length, 3)
318 |
319 | // please let's do better!
320 | assert.isDefined(nodes['function () {\n return 1;\n }22074'])
321 | })
322 |
323 | test("doesn't duplicate nodes if they are different", () => {
324 | const foo$ = (state) => state.foo // node1
325 | const select = () => 1 // node 2
326 | createSelector(foo$, select)
327 | createSelector(select) // node 3
328 | registerSelectors({ foo$, baz: select })
329 | const { nodes } = selectorGraph()
330 | assert.equal(Object.keys(nodes).length, 2)
331 | })
332 |
333 | test('it names the nodes based on entries in the registry if they are there', () => {
334 | createMockSelectors()
335 | const { edges } = selectorGraph()
336 |
337 | const expectedEdges = [
338 | { from: 'users$', to: 'data$' },
339 | { from: 'pets$', to: 'data$' },
340 | { from: 'currentUser$', to: 'ui$' },
341 | { from: 'currentUser$', to: 'users$' },
342 | { from: 'currentUserPets$', to: 'currentUser$' },
343 | { from: 'currentUserPets$', to: 'pets$' },
344 | { from: 'thingy$', to: 'random$' },
345 | { from: 'booya$', to: 'thingy$' },
346 | { from: 'booya$', to: 'currentUser$' }
347 | ]
348 | assert.sameDeepMembers(edges, expectedEdges)
349 | })
350 | })
351 | })
352 |
--------------------------------------------------------------------------------