zalmoxisus / redux-devtools-test-generator

Generate tests right in Redux DevTools.

Home Page:https://youtu.be/cbXLohVbzNI?t=392

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Work with Immutable.JS

cristian-sima opened this issue · comments

It seems that inside the templates (doesn't matter which one), there are injected the prevState and curState, but these are serialized. In the case I have some Immutable JS objects, they will be transformed to plain JS.

A solution will be to be given the original state (originalPrevState and originalCurState) which are the Immutable objects

Could you provide an example template you'd use with those objects?

Thanks for looking into it!

Sure, but I'll give it tomorrow cause it's too late now

There's no hurry. Anyway, in the current implementation it's not possible to have the original objects as they are stringified before sending to the extension. But I'm working on another way of sending data.

@zalmoxisus Sorry for delay.

Essentially, it is pretty simple. For any example which includes Immutable.JS anywhere in the action or state, redux-devtools-test-generator will produce code which breaks.

In this example I normalize some invoices. I use 2 reducers:

  1. all stores their IDs as an Immutable.List
  2. byIDs stores their content as a Immutable.Map

The companyInvoices reducer just stores the all and byIDs reducers.

For simplicity, I react to just one action: COMPANY_ADD_INVOICE.

import { combineReducers } from "redux";
import * as Immutable from "immutable";

const COMPANY_ADD_INVOICE = "COMPANY_ADD_INVOICE";

return combineReducers({
  companyInvoices: combineReducers({
    all: (state = Immutable.List(), action) => {
      const { payload } = action;

      switch (action.type) {

        case COMPANY_ADD_INVOICE: {
          const invoice = payload,
            id = String(invoice.get("ID"));

          return {
            ...state,
            IDs: state.IDs.push(id),
          };
        }
        default:
          return state;
      }
    },
    byIDs: (state = Immutable.Map(), action) => {
      const { payload } = action;

      switch (action.type) {

        case COMPANY_ADD_INVOICE: {
          const invoice = payload;

          return state.set(String(invoice.get("ID")), invoice);
        }
        default:
          return state;
      }
    },
  }),
});

const initialState = {
  companyInvoices: {
    all   : Immutable.List(),
    byIDs : Immutable.Map(),
  },
};

// now I dispatch an action
dispatch({
  type    : COMPANY_ADD_INVOICE,
  payload : Immutable.Map({
    ID          : 1,
    Description : "T.V. Subscription",
    Price       : 49.99,
  }),
});

const afterState= {
  companyInvoices: {
    all   : Immutable.List(["1"]),
    byIDs : Immutable.Map({
      "1": Immutable.Map({
        ID          : 1,
        Description : "T.V. Subscription",
        Price       : 49.99,
      }),
    }),
  },
};

When using redux-devtools-test-generator the state will be serialized into:

const initialState = {
  companyInvoices: {
    all   : [],
    byIDs : {},
  },
};

dispatch({
  type    : COMPANY_ADD_INVOICE,
  payload : {
    ID          : 1,
    Description : "T.V. Subscription",
    Price       : 49.99,
  },
});

const afterState = {
  companyInvoices: {
    all   : ["1"],
    byIDs : {
      1: {
        ID          : 1,
        Description : "T.V. Subscription",
        Price       : 49.99,
      },
    },
  },
};

which is inserted into Jest template

import reducers from "../../reducers";

test("reducers", () => {
  let state;

  state = reducers({
    companyInvoices: {
      all   : [],
      byIDs : {},
    },
  }, {
    type    : "COMPANY_ADD_INVOICE",
    payload : {
      ID          : 1,
      Description : "T.V. Subscription",
      Price       : 49.99,
    },
  });
  expect(state).toEqual({
    companyInvoices: {
      all   : ["1"],
      byIDs : {
        1: {
          ID          : 1,
          Description : "T.V. Subscription",
          Price       : 49.99,
        },
      },
    },
  });
});

which breaks everything because:

  1. action is deserialed and then when doing payload.get("ID") we'll get an payload.get is not a function error
  2. initial state is not the same as the initial state deserialed
  3. after state is not the same as the after state deserialed

An approach is to provide originalAction, originalInitialState and originalAfterState.

This example is pretty simple, however my apps are structured like this for everything

@cristian-sima, thanks for the details!

I still don't see how providing originalAction, originalInitialState and originalAfterState could help to have a better template.

A solution would be to add fromJS and toJS in the template, but of course it requires that everything including the action object would be Immutable.

Another solution is to indicate how to serialize the data using serializeAction and serializeState parameters. For example:

const customSerializer = (key, value) => (
    value && Map.isMap(value) ? `Map(${value.toJS()})` : value
);

const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
  serializeState: customSerializer,
  serializeAction: customSerializer,
}));

So, instead of serializing maps into plain objects like { a: 1 } , you'll have Map({ a: 1 }). You can add other types or even use iterables instead of plain objects.

Sorry again for the great delay. I'll answer soon :D