Browse Source

video14: test Redux Connect components.

Frederic G. MARAND 6 years ago
parent
commit
5617ad3286
9 changed files with 317 additions and 5 deletions
  1. 3 0
      .eslintrc.js
  2. 3 1
      package.json
  3. 61 0
      src/Todos.css
  4. 65 0
      src/Todos.js
  5. 61 0
      src/Todos.test.js
  6. 30 0
      src/__snapshots__/Todos.test.js.snap
  7. 14 2
      src/index.js
  8. 48 0
      src/store.js
  9. 32 2
      yarn.lock

+ 3 - 0
.eslintrc.js

@@ -14,6 +14,9 @@ module.exports = {
     "ReactDOM": true,
     "_": true,
 
+    // React
+    "connect": false,
+
     // Test tools: Jest
     "after": false,
     "afterEach": false,

+ 3 - 1
package.json

@@ -3,7 +3,9 @@
     "prop-types": "^15.6.1",
     "raf": "^3.4.0",
     "react": "^16.2.0",
-    "react-dom": "^16.2.0"
+    "react-dom": "^16.2.0",
+    "react-redux": "^5.0.7",
+    "redux": "^4.0.0"
   },
   "devDependencies": {
     "enzyme": "^3.3.0",

+ 61 - 0
src/Todos.css

@@ -0,0 +1,61 @@
+body {
+  background-color: white;
+  box-sizing: border-box;
+  color: #81C199;
+  padding-top: 3em;
+}
+
+/**
+ * @see https://www.paulirish.com/2012/box-sizing-border-box-ftw/
+ */
+*, *:before, *:after {
+  box-sizing: inherit;
+}
+
+h1 {
+  font-size: 150%;
+  font-weight: normal;
+  letter-spacing: 0.2em;
+  margin-left: auto;
+  margin-right: auto;
+  text-align: center;
+  text-transform: uppercase;
+}
+
+form {
+  margin-left: 1em;
+  margin-right: 1em;
+}
+
+form button,
+form input {
+}
+
+form button[type="submit"] {
+  background-color: #8bd7a9;
+  border-radius: 1em 1em;
+  border-style: none;
+  color: white; /* user-agent says black */
+  display: block;
+  font-size: 80%;
+  letter-spacing: 0.05em;
+  margin: 1.5em 0;
+  padding: 0.5em;
+  text-transform: uppercase;
+  width: 100%;
+}
+
+form input[type="text"] {
+  border-color: #81C199;
+  border-radius: 1em 1em;
+  border-style: solid;
+  border-width: 1px;
+  display: block;
+  margin: 1.5em 0;
+  padding: 0.5em 0 0.5em 1em;
+  width: 100%;
+}
+
+li {
+  list-style: none;
+}

+ 65 - 0
src/Todos.js

@@ -0,0 +1,65 @@
+import React, { Component, Fragment } from "react";
+import "./Todos.css";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+import { addTodo, removeTodo } from "./store";
+
+class TodoList extends Component {
+  state = {
+    input: "",
+  };
+
+  handleclick = i => () =>
+    this.props.removeTodo(i);
+
+  handleChange = e =>
+    this.setState({ input: e.currentTarget.value });
+
+  handleSubmit = (e) => {
+    e.preventDefault();
+    this.props.addTodo({ text: this.state.input });
+    this.setState({ input: "" });
+  };
+
+  render() {
+    return (
+      <form className="todos--container" data-testid="addTodoForm" onSubmit={this.handleSubmit}>
+        <h1 className="todos--h1">Todos</h1>
+        <input type="text" name="todo" data-testid="todo"
+          onChange={this.handleChange}
+          value={this.state.input} />
+        <ul>
+          {this.props.todos.map(({ text }, i) => (
+            <li onClick={this.handleclick(i)} key={i}>
+              {text}
+            </li>
+          ))}
+        </ul>
+        <button className="todo--button" type="submit" data-testid="submitButton">Add todo</button>
+      </form>
+    );
+  }
+}
+
+TodoList.propTypes = {
+  addTodo: PropTypes.func,
+  removeTodo: PropTypes.func,
+  todos: PropTypes.array,
+};
+
+const mapStateToProps = ({ currentList: { todos } }) => ({ todos });
+
+const bindActionsToDispatch = dispatch => ({
+  addTodo: todo => dispatch(addTodo(todo)),
+  removeTodo: id => dispatch(removeTodo(id)),
+});
+
+const TodoListContainer = connect(
+  mapStateToProps,
+  bindActionsToDispatch
+)(TodoList);
+
+export {
+  TodoListContainer,
+  TodoList
+};

+ 61 - 0
src/Todos.test.js

@@ -0,0 +1,61 @@
+import React from "react";
+// Since we're not testing Redux, but our use of it, we can just use the component
+// itself instead of mocking Redux.
+import { TodoList } from "./Todos";
+import { shallow, configure } from "enzyme";
+import Adapter from "enzyme-adapter-react-16";
+import toJson from "enzyme-to-json";
+
+configure({ adapter: new Adapter() });
+
+const nullFn = () => null;
+const mockSubmitEvent = { preventDefault: nullFn };
+
+// Tests
+// - addTodo with button click
+// - removeTodo with li click
+// - matches snapshot
+
+describe("<TodoList />", () => {
+  let mockAdd;
+  let mockRemove;
+
+  beforeEach(() => {
+    mockAdd = jest.fn();
+    mockRemove = jest.fn();
+  });
+
+  it("should call addTodo (Redux action creator) with proper values", () => {
+    const props = {
+      addTodo: mockAdd,
+      todos: [],
+    };
+    const wrapper = shallow(<TodoList {...props} />);
+    const expected = "Buy groceries";
+    wrapper.find("input").simulate("change", { currentTarget: { value: expected } });
+    // We could check the operation of the onChange, BTW.
+
+    const form = wrapper.find("[data-testid='addTodoForm']");
+    form.simulate("submit", mockSubmitEvent);
+    expect(props.addTodo).toHaveBeenCalledWith({ text: expected });
+  });
+
+  it("should call removeTodo (Redux action creator) on li click", () => {
+    const props = {
+      removeTodo: mockRemove,
+      todos: [{ text: "Buy groceries" }, { text: "Change oil" }],
+    };
+    const wrapper = shallow(<TodoList {...props} />);
+    const expectedOffset = 0;
+    const li = wrapper.find("li").at(expectedOffset);
+    const actualOffset = parseInt(li.key(), 10);
+    expect(actualOffset).toBe(expectedOffset);
+    li.simulate("click");
+    expect(props.removeTodo).toHaveBeenCalledWith(expectedOffset);
+  });
+
+  it("should match snapshot", () => {
+    const wrapper = shallow(<TodoList todos={[]} />);
+    expect(toJson(wrapper)).toMatchSnapshot();
+  });
+});

+ 30 - 0
src/__snapshots__/Todos.test.js.snap

@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<TodoList /> should match snapshot 1`] = `
+<form
+  className="todos--container"
+  data-testid="addTodoForm"
+  onSubmit={[Function]}
+>
+  <h1
+    className="todos--h1"
+  >
+    Todos
+  </h1>
+  <input
+    data-testid="todo"
+    name="todo"
+    onChange={[Function]}
+    type="text"
+    value=""
+  />
+  <ul />
+  <button
+    className="todo--button"
+    data-testid="submitButton"
+    type="submit"
+  >
+    Add todo
+  </button>
+</form>
+`;

+ 14 - 2
src/index.js

@@ -2,9 +2,21 @@ import React from "react";
 import ReactDOM from "react-dom";
 import "./index.css";
 // import { App } from "./App";
-import { Form } from "./Form";
+// import { Form } from "./Form";
+import { Provider } from "react-redux";
+import { store } from "./store";
+
 import registerServiceWorker from "./registerServiceWorker";
+import { TodoListContainer as App } from "./Todos";
 
 // ReactDOM.render(<App />, document.getElementById("root"));
-ReactDOM.render(<Form />, document.getElementById("root"));
+// ReactDOM.render(<Form />, document.getElementById("root"));
+
+ReactDOM.render(
+  <Provider store={store}>
+    <App />
+  </Provider>,
+  document.getElementById("root")
+);
+
 registerServiceWorker();

+ 48 - 0
src/store.js

@@ -0,0 +1,48 @@
+import { combineReducers, createStore } from "redux";
+
+const addTodo = (todo) => ({
+  type: "ADD_TODO",
+  todo,
+});
+
+const removeTodo = (id) => ({
+  type: "REMOVE_TODO",
+  id,
+});
+
+const initialState = {
+  todos: [],
+};
+
+const handleNewTodo = (state, action) => ({
+  todos: [...state.todos, action.todo],
+});
+
+const handleRemoveTodo = (state, action) => ({
+  todos: [
+    ...state.todos.slice(0, action.id),
+    ...state.todos.slice(action.id + 1),
+  ],
+});
+
+const currentList = (state = initialState, action) => {
+  const handlers = {
+    ADD_TODO: handleNewTodo,
+    REMOVE_TODO: handleRemoveTodo,
+  };
+
+  return handlers[action.type] ? handlers[action.type](state, action) : state;
+};
+
+const rootReducer = combineReducers({
+  currentList,
+});
+
+const store = createStore(rootReducer);
+
+export {
+  addTodo,
+  removeTodo,
+  store,
+};
+

+ 32 - 2
yarn.lock

@@ -3239,6 +3239,10 @@ hoek@4.x.x:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
 
+hoist-non-react-statics@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
+
 home-or-tmp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -3488,7 +3492,7 @@ interpret@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
 
-invariant@^2.2.2:
+invariant@^2.0.0, invariant@^2.2.2:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
   dependencies:
@@ -4345,6 +4349,10 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
+lodash-es@^4.17.5:
+  version "4.17.10"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
+
 lodash._reinterpolate@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -4386,7 +4394,7 @@ lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 
-"lodash@>=3.5 <5", lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0:
+"lodash@>=3.5 <5", lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
@@ -5782,6 +5790,17 @@ react-reconciler@^0.7.0:
     object-assign "^4.1.1"
     prop-types "^15.6.0"
 
+react-redux@^5.0.7:
+  version "5.0.7"
+  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
+  dependencies:
+    hoist-non-react-statics "^2.5.0"
+    invariant "^2.0.0"
+    lodash "^4.17.5"
+    lodash-es "^4.17.5"
+    loose-envify "^1.1.0"
+    prop-types "^15.6.0"
+
 react-scripts@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-1.1.1.tgz#279d449f7311fed910506987a1ade014027788a8"
@@ -5931,6 +5950,13 @@ reduce-function-call@^1.0.1:
   dependencies:
     balanced-match "^0.4.2"
 
+redux@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03"
+  dependencies:
+    loose-envify "^1.1.0"
+    symbol-observable "^1.2.0"
+
 regenerate@^1.2.1:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
@@ -6696,6 +6722,10 @@ sw-toolbox@^3.4.0:
     path-to-regexp "^1.0.1"
     serviceworker-cache-polyfill "^4.0.0"
 
+symbol-observable@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+
 symbol-tree@^3.2.1:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"