Repository: ViacomInc/data-point
Pull request #383 (2019-08-21 20:50:31)

Reducer object tests
<!--
Thanks for your interest in the project. We appreciate bugs filed and PRs submitted!

Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).

Also, please make sure you're familiar with and follow the instructions in the
contributing guidelines (found in the CONTRIBUTING.md file).

Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!

BREAKING CHANGES:

If your PR includes a breaking change, please submit it with a codemod under
data-point-codemods that will help users upgrade their codebase.

Breaking changes without a codemod will not be accepted unless a codemod is not
viable or does not apply to the specific situation.
-->

<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**:

- Adds missing tests for ReducerObject
- creates nee Reducer.isReducer method
- adds support back for constant reducer

re #357

<!-- Have you done all of these things?  -->
**Checklist**:
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
- [ ] Has Breaking changes
- [ ] Documentation
- [x] Tests
- [x] Ready to be merged <!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
- [ ] Added username to **all-contributors** list

<!-- feel free to add additional comments -->


codecov[bot]: # [Codecov](https://codecov.io/gh/ViacomInc/data-point/pull/383?src=pr&el=h1) Report
> Merging [#383](https://codecov.io/gh/ViacomInc/data-point/pull/383?src=pr&el=desc) into [canary](https://codecov.io/gh/ViacomInc/data-point/commit/0d46d7c2042c0c3dde71cea5fd72828806ff0f27?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/ViacomInc/data-point/pull/383/graphs/tree.svg?width=650&token=LYxItnrx4i&height=150&src=pr)](https://codecov.io/gh/ViacomInc/data-point/pull/383?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           canary   #383   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          11     15    +4     
  Lines         226    269   +43     
  Branches       35     40    +5     
=====================================
+ Hits          226    269   +43
```


| [Impacted Files](https://codecov.io/gh/ViacomInc/data-point/pull/383?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/data-point/src/Reducer.js](https://codecov.io/gh/ViacomInc/data-point/pull/383/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvUmVkdWNlci5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [...s/data-point/src/reducer-helpers/constant/index.js](https://codecov.io/gh/ViacomInc/data-point/pull/383/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvcmVkdWNlci1oZWxwZXJzL2NvbnN0YW50L2luZGV4Lmpz) | `100% <100%> (ø)` | |
| [packages/data-point/src/is-reducer-constant.js](https://codecov.io/gh/ViacomInc/data-point/pull/383/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvaXMtcmVkdWNlci1jb25zdGFudC5qcw==) | `100% <100%> (ø)` | |
| [packages/data-point/src/ReducerObject.js](https://codecov.io/gh/ViacomInc/data-point/pull/383/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvUmVkdWNlck9iamVjdC5qcw==) | `100% <100%> (ø)` | |
| [...ata-point/src/reducer-helpers/constant/constant.js](https://codecov.io/gh/ViacomInc/data-point/pull/383/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvcmVkdWNlci1oZWxwZXJzL2NvbnN0YW50L2NvbnN0YW50Lmpz) | `100% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ViacomInc/data-point/pull/383?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ViacomInc/data-point/pull/383?src=pr&el=footer). Last update [0d46d7c...b7f0d84](https://codecov.io/gh/ViacomInc/data-point/pull/383?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/data-point/src/ReducerObject.js b/packages/data-point/src/ReducerObject.js
index c0fa29a..9878c94 100644
--- a/packages/data-point/src/ReducerObject.js
+++ b/packages/data-point/src/ReducerObject.js
@@ -1,66 +1,63 @@
-const isPlainObject = require("lodash/isPlainObject");
 const set = require("lodash/set");
 const cloneDeep = require("lodash/cloneDeep");
 
-const { ReducerNative } = require("./ReducerNative");
+const { Reducer } = require("./Reducer");
+const isReducerConstant = require("./is-reducer-constant");
+
+function isPlainObject(obj) {
+  return (
+    typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype
+  );
+}
 
-/**
- * @return {Object}
- */
 function newProps() {
   return {
-    constants: {},
+    constant: {},
     reducers: []
   };
 }
 
 /**
+ * NOTE: This function makes recursive calls to itself
  * @param {Function} createReducer
  * @param {Object} source
  * @param {Array} stack
  * @param {Object} props
- * @returns {Array}
+ * @returns {Object} a props object
  */
-function getProps(createReducer, source, stack = [], props = newProps()) {
+function getObjectProperties(
+  createReducer,
+  source,
+  stack = [],
+  props = newProps()
+) {
   Object.keys(source).forEach(key => {
     const path = stack.concat(key);
     const value = source[key];
+
+    if (isReducerConstant(value)) {
+      set(props.constant, path, value.constantValue);
+      return;
+    }
+
     if (isPlainObject(value)) {
       // NOTE: recursive call
-      getProps(createReducer, value, path, props);
+      getObjectProperties(createReducer, value, path, props);
       return;
     }
 
     const reducer = createReducer(value);
-    // TODO: should we implement ReducerConstant?
-    if (reducer.type === "ReducerConstant") {
-      set(props.constants, path, reducer.value);
-    } else {
-      props.reducers.push({ path, reducer });
-    }
+    props.reducers.push({ path, reducer });
   });
 
   return props;
 }
 
-/**
- * @param {Object} source
- * @return {Function}
- */
-function getSourceFunction(source) {
-  const fn = () => cloneDeep(source);
-  Object.defineProperty(fn, "name", { value: "source" });
-  return fn;
-}
-
-class ReducerObject extends ReducerNative {
+class ReducerObject extends Reducer {
   constructor(spec, createReducer) {
     super("object", undefined, spec);
 
-    const props = getProps(createReducer, spec);
-
-    this.source = getSourceFunction(props.constants);
-    this.reducers = props.reducers;
+    this.objectProperties = getObjectProperties(createReducer, spec);
   }
 
   static isType(spec) {
@@ -68,15 +65,16 @@ class ReducerObject extends ReducerNative {
   }
 
   /**
-   * @param {Object} manager
-   * @param {Function} resolveReducer
    * @param {Accumulator} accumulator
-   * @param {ReducerObject} reducer
+   * @param {Function} resolveReducer
    * @returns {Promise}
    */
   async resolve(accumulator, resolveReducer) {
-    const reducers = this.reducers;
-    const result = this.source();
+    const reducers = this.objectProperties.reducers;
+
+    // clone constant value so any subsequent change to the object
+    // is not treated as a reference to the original source
+    const result = cloneDeep(this.objectProperties.constant);
 
     if (reducers.length === 0) {
       return result;
@@ -94,8 +92,7 @@ class ReducerObject extends ReducerNative {
 }
 
 module.exports = {
-  newProps,
-  getProps,
-  getSourceFunction,
+  isPlainObject,
+  getObjectProperties,
   ReducerObject
 };


Test patch:
diff --git a/packages/data-point/src/ReducerObject.test.js b/packages/data-point/src/ReducerObject.test.js
new file mode 100644
index 0000000..212b712
--- /dev/null
+++ b/packages/data-point/src/ReducerObject.test.js
@@ -0,0 +1,243 @@
+const reducerObject = require("./ReducerObject");
+const { Accumulator } = require("./Accumulator");
+
+const constant = require("./reducer-helpers/constant");
+
+const mockCreateReducer = jest.fn(spec => {
+  return (value, acc) => spec(value, acc);
+});
+
+const mockResolveReducer = (acc, reducer) => {
+  return reducer(acc.value, acc);
+};
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+describe("isPlainObject", () => {
+  it("should return true for plain objects", () => {
+    expect(reducerObject.isPlainObject({})).toEqual(true);
+    expect(reducerObject.isPlainObject("string")).toEqual(false);
+    expect(reducerObject.isPlainObject(123)).toEqual(false);
+    expect(reducerObject.isPlainObject(() => true)).toEqual(false);
+
+    expect(reducerObject.isPlainObject(Object.create({}))).toEqual(false);
+    class Test {}
+    expect(reducerObject.isPlainObject(new Test())).toEqual(false);
+  });
+});
+
+describe("getObjectProperties", () => {
+  it("should extract all constant", () => {
+    const obj = {
+      prop1: constant("prop1"),
+      prop2: constant("prop2"),
+      prop3: constant({
+        a: "a"
+      })
+    };
+
+    const props = reducerObject.getObjectProperties(mockCreateReducer, obj);
+    expect(props.constant).toEqual({
+      prop1: "prop1",
+      prop2: "prop2",
+      prop3: {
+        a: "a"
+      }
+    });
+  });
+
+  it("should extract reducer props", () => {
+    const obj = {
+      prop1: () => true,
+      prop2: () => true
+    };
+
+    const props = reducerObject.getObjectProperties(mockCreateReducer, obj);
+    expect(props.reducers).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "path": Array [
+            "prop1",
+          ],
+          "reducer": [Function],
+        },
+        Object {
+          "path": Array [
+            "prop2",
+          ],
+          "reducer": [Function],
+        },
+      ]
+    `);
+  });
+
+  it("should crawl recursive for nested plainObject properties", () => {
+    const obj = {
+      nested: {
+        nestedDeep: {
+          prop1: () => true
+        }
+      }
+    };
+
+    const props = reducerObject.getObjectProperties(mockCreateReducer, obj);
+    // nested prop should match path:
+    expect(props.reducers[0].path).toEqual(["nested", "nestedDeep", "prop1"]);
+    // matching entire reducer structure
+    expect(props.reducers).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "path": Array [
+            "nested",
+            "nestedDeep",
+            "prop1",
+          ],
+          "reducer": [Function],
+        },
+      ]
+    `);
+  });
+
+  it("should parse constant, reducers and plain objects", () => {
+    const obj = {
+      prop1: constant({
+        c: "constant"
+      }),
+      nested: {
+        prop2: () => true,
+        nestedDeep: {
+          prop3: () => true
+        }
+      }
+    };
+
+    const props = reducerObject.getObjectProperties(mockCreateReducer, obj);
+
+    expect(props).toMatchInlineSnapshot(`
+      Object {
+        "constant": Object {
+          "prop1": Object {
+            "c": "constant",
+          },
+        },
+        "reducers": Array [
+          Object {
+            "path": Array [
+              "nested",
+              "prop2",
+            ],
+            "reducer": [Function],
+          },
+          Object {
+            "path": Array [
+              "nested",
+              "nestedDeep",
+              "prop3",
+            ],
+            "reducer": [Function],
+          },
+        ],
+      }
+    `);
+  });
+});
+
+describe("ReducerObject", () => {
+  const { ReducerObject } = reducerObject;
+  describe("constructor", () => {
+    it("should set type to list", () => {
+      const reducer = new ReducerObject({}, mockCreateReducer);
+      expect(reducer.type).toEqual("object");
+    });
+
+    it("should have no name defined", () => {
+      const reducer = new ReducerObject({}, mockCreateReducer);
+      expect(reducer.name).toEqual(undefined);
+    });
+
+    it("should parse source and set to objectProperties property", () => {
+      const reducer = new ReducerObject(
+        {
+          prop1: constant("prop1"),
+          prop2: () => true
+        },
+        mockCreateReducer
+      );
+      expect(reducer.objectProperties.constant).toEqual({
+        prop1: "prop1"
+      });
+
+      expect(reducer.objectProperties.reducers).toHaveLength(1);
+    });
+  });
+
+  describe("isType", () => {
+    it("should return true if source is plain object, false for non", () => {
+      // rest of tests are under the isPlainObject unit tests
+      expect(ReducerObject.isType({})).toEqual(true);
+      expect(ReducerObject.isType(Object.create({}))).toEqual(false);
+    });
+  });
+
+  describe("resolve", () => {
+    it("should resolve object", async () => {
+      const reducer = new ReducerObject(
+        {
+          prop1: constant("prop1"),
+          prop2: val => val.prop2,
+          prop3: {
+            prop4: constant("prop4"),
+            prop5: val => val.prop5
+          }
+        },
+        mockCreateReducer
+      );
+
+      const acc = new Accumulator({
+        value: {
+          prop2: "prop2",
+          prop5: "prop5"
+        }
+      });
+      const result = await reducer.resolve(acc, mockResolveReducer);
+
+      expect(result).toEqual({
+        prop1: "prop1",
+        prop2: "prop2",
+        prop3: {
+          prop4: "prop4",
+          prop5: "prop5"
+        }
+      });
+    });
+
+    it("should deepClone constant values", async () => {
+      const testConstant = {
+        constant: "constant"
+      };
+
+      const reducer = new ReducerObject(
+        {
+          prop1: constant(testConstant)
+        },
+        mockCreateReducer
+      );
+
+      const acc = new Accumulator();
+      const result = await reducer.resolve(acc, mockResolveReducer);
+
+      expect(result).toEqual({
+        prop1: {
+          constant: "constant"
+        }
+      });
+
+      result.prop1.mutateProp = true;
+      expect(testConstant).toEqual({
+        constant: "constant"
+      });
+    });
+  });
+});