Browse Source

video14: test React forms.

Frederic G. MARAND 6 years ago
parent
commit
f13942d3bd
8 changed files with 289 additions and 3 deletions
  1. 5 0
      .eslintrc.js
  2. 1 1
      .idea/runConfigurations/Jest.xml
  3. 63 0
      src/Form.css
  4. 60 0
      src/Form.js
  5. 83 0
      src/Form.test.js
  6. 61 0
      src/__snapshots__/Form.test.js.snap
  7. 12 0
      src/api.js
  8. 4 2
      src/index.js

+ 5 - 0
.eslintrc.js

@@ -15,6 +15,10 @@ module.exports = {
     "_": true,
 
     // Test tools: Jest
+    "after": false,
+    "afterEach": false,
+    "before": false,
+    "beforeEach": false,
     "describe": false,
     "expect": false,
     "it": false,
@@ -24,6 +28,7 @@ module.exports = {
 
   "plugins": ["react"],
 
+  "parser": "babel-eslint",
   "parserOptions": {
     "ecmaFeatures": {
       "arrowFunctions": true,

+ 1 - 1
.idea/runConfigurations/Jest.xml

@@ -3,7 +3,7 @@
     <node-interpreter value="project" />
     <node-options value="" />
     <working-dir value="$PROJECT_DIR$" />
-    <jest-options value="--watchAll --env=jsdom" />
+    <jest-options value="--watchAll --env=jsdom --verbose" />
     <envs />
     <scope-kind value="ALL" />
     <method />

+ 63 - 0
src/Form.css

@@ -0,0 +1,63 @@
+body {
+  background-color: #4b4c66;
+  box-sizing: border-box;
+  color: white;
+  padding-top: 3em;
+}
+
+/**
+ * @see https://www.paulirish.com/2012/box-sizing-border-box-ftw/
+ */
+*, *:before, *:after {
+  box-sizing: inherit;
+}
+
+h2 {
+  margin-left: auto;
+  margin-right: auto;
+  text-align: center;
+  text-transform: uppercase;
+}
+
+form {
+  margin-left: 10%;
+  margin-right: 10%;
+}
+
+form button,
+form input {
+  border-style: none;
+  border-width: 0;
+}
+
+form input[type="checkbox"] {
+  display: inline;
+  margin: 1em;
+  padding: 0.5em;
+}
+form .form-item {
+  margin-left: auto;
+  margin-right: auto;
+  text-align: center;
+}
+
+form button[type="submit"] {
+  background-color: #2b50f4;
+  border-radius: 1em 1em;
+  color: white; /* user-agent says black */
+  display: block;
+  font-size: 100%;
+  letter-spacing: 0.2em;
+  margin: 1.5em 0;
+  padding: 0.5em;
+  text-transform: uppercase;
+  width: 100%;
+}
+
+form input[type="text"] {
+  border-radius: 0.8em 0.8em;
+  display: block;
+  margin: 1.5em 0;
+  padding: 0.5em 0 0.5em 1em;
+  width: 100%;
+}

+ 60 - 0
src/Form.js

@@ -0,0 +1,60 @@
+import React, { Component } from "react";
+import "./Form.css";
+import { api } from "./api";
+
+class Form extends Component {
+  state = {
+    name: "",
+    email: "",
+    number: "",
+    optIn: true,
+  };
+
+  handleChange = (str) => {
+    return e =>
+      this.setState({ [str]: e.currentTarget.value });
+  };
+
+  handleSubmit = (e) => {
+    e.preventDefault();
+    api.addUser(this.state.name, this.state.email, this.state.number);
+  };
+
+  handlePromotionClick = () => {
+    this.setState({ optIn: !this.state.optIn });
+  };
+
+  render() {
+    return (
+      // The data-testid attributes give us more security for tests, than those use for styling.
+      <form data-testid="addUserForm" onSubmit={this.handleSubmit}>
+        <h2>Request information</h2>
+        <input type="text" name="name" data-testid="name"
+          onChange={this.handleChange("name")}
+          placeholder="Name"
+          value={this.state.name} />
+        <input type="text" name="email" data-testid="email"
+          onChange={this.handleChange("email")}
+          placeholder="Email"
+          value={this.state.email} />
+        <input type="text" name="number" data-testid="number"
+          onChange={this.handleChange("number")}
+          placeholder="Number"
+          value={this.state.number} />
+
+        <div className="form-item">
+          <input type="checkbox" name="promo" id="promo" data-testid="checked"
+            checked={this.state.optIn}
+            onClick={this.handlePromotionClick}/>
+          <label data-testid="promotionsP" className="promotions" htmlFor="promo">Receive promotions</label>
+        </div>
+
+        <button type="submit" data-testid="submitButton">Submit</button>
+      </form>
+    );
+  }
+}
+
+export {
+  Form
+};

+ 83 - 0
src/Form.test.js

@@ -0,0 +1,83 @@
+import React from "react";
+import { Form } from "./Form";
+import { shallow, configure } from "enzyme";
+import Adapter from "enzyme-adapter-react-16";
+import toJson from "enzyme-to-json";
+import { api } from "./api";
+
+configure({ adapter: new Adapter() });
+
+const makeChangeEvent = value => ({ currentTarget: { value } });
+
+const nullFn = () => null;
+
+const updateInput = (wrapper, instance, newValue) => {
+  const input = wrapper.find(instance);
+  input.simulate("change", makeChangeEvent(newValue));
+
+  // Now updated by the event.
+  return wrapper.find(instance);
+};
+
+describe("<Form />", () => {
+
+  let wrapper;
+
+  beforeEach(() => {
+    wrapper = shallow(<Form />);
+  });
+
+  // opted-in by default: have to opt-out (!)
+  // actually input their information
+  // submits the form, calls the api method
+  // matches snapshot
+  it("should receive default promotions as true", () => {
+    const promotionInput = wrapper.find("[data-testid='checked']");
+    expect(promotionInput.props().checked).toBe(true);
+  });
+
+  it("should allow users to fill out form", () => {
+    const expectations = {
+      "name": "Tyler",
+      "email": "tyler@durden.me",
+      "number": "42", // User inputs in test fields are strings, even when numeric.
+    };
+    const actual = {};
+
+    for (const [input, expected] of Object.entries(expectations)) {
+      actual[input] = updateInput(wrapper, `[data-testid="${input}"]`, expected);
+    }
+
+    for (const [input, expected] of Object.entries(expectations)) {
+      expect(actual[input].props().value).toBe(expected);
+      expect(actual[input].getElement().props.value).toBe(expected);
+    }
+    const promotionInputPre = wrapper.find("[data-testid='checked']");
+    promotionInputPre.simulate("click");
+
+    const promotionInputPost = wrapper.find("[data-testid='checked']");
+    expect(promotionInputPost.props().checked).toBe(false);
+  });
+
+  it("should submit the form to the API", () => {
+    const expectations = {
+      "name": "Tyler",
+      "email": "tyler@durden.me",
+      "number": "42", // User inputs in test fields are strings, even when numeric.
+    };
+
+    for (const [input, expected] of Object.entries(expectations)) {
+      updateInput(wrapper, `[data-testid="${input}"]`, expected);
+    }
+
+    // Ensure addUser is called on submit, but we're not testing the API, so mock it.
+    jest.spyOn(api, "addUser").mockImplementation(() => Promise.resolve({ data: "New user added" }));
+    const submit = wrapper.find("[data-testid='addUserForm']");
+    submit.simulate("submit", { preventDefault: nullFn });
+    expect(api.addUser).toHaveBeenCalledWith(...Object.values(expectations));
+  });
+
+  it("should match snapshot", () => {
+    expect(toJson(wrapper)).toMatchSnapshot();
+  });
+});

+ 61 - 0
src/__snapshots__/Form.test.js.snap

@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Form /> should match snapshot 1`] = `
+<form
+  data-testid="addUserForm"
+  onSubmit={[Function]}
+>
+  <h2>
+    Request information
+  </h2>
+  <input
+    data-testid="name"
+    name="name"
+    onChange={[Function]}
+    placeholder="Name"
+    type="text"
+    value=""
+  />
+  <input
+    data-testid="email"
+    name="email"
+    onChange={[Function]}
+    placeholder="Email"
+    type="text"
+    value=""
+  />
+  <input
+    data-testid="number"
+    name="number"
+    onChange={[Function]}
+    placeholder="Number"
+    type="text"
+    value=""
+  />
+  <div
+    className="form-item"
+  >
+    <input
+      checked={true}
+      data-testid="checked"
+      id="promo"
+      name="promo"
+      onClick={[Function]}
+      type="checkbox"
+    />
+    <label
+      className="promotions"
+      data-testid="promotionsP"
+      htmlFor="promo"
+    >
+      Receive promotions
+    </label>
+  </div>
+  <button
+    data-testid="submitButton"
+    type="submit"
+  >
+    Submit
+  </button>
+</form>
+`;

+ 12 - 0
src/api.js

@@ -0,0 +1,12 @@
+class Api {
+  users = [];
+
+  addUser(name, email, number) {
+    const user = { name, email, number };
+    this.users.push(user);
+    return user;
+  }
+}
+
+const api = new Api();
+export { api };

+ 4 - 2
src/index.js

@@ -1,8 +1,10 @@
 import React from "react";
 import ReactDOM from "react-dom";
 import "./index.css";
-import { App } from "./App";
+// import { App } from "./App";
+import { Form } from "./Form";
 import registerServiceWorker from "./registerServiceWorker";
 
-ReactDOM.render(<App />, document.getElementById("root"));
+// ReactDOM.render(<App />, document.getElementById("root"));
+ReactDOM.render(<Form />, document.getElementById("root"));
 registerServiceWorker();