Skip to content

Commit

Permalink
#000 okan add tests, clean up code and build scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
okan.cetin committed Sep 13, 2016
1 parent 53bfc79 commit ef8453c
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 74 deletions.
2 changes: 1 addition & 1 deletion SpecRunner.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<head>
<meta charset="utf-8">
<title>Cow tests</title>
<title>redux-ts tests</title>
<link rel="stylesheet" media="all" href="./node_modules/mocha/mocha.css">
</head>

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
"build:prod:min": "cross-env NODE_ENV=prod webpack -p src/index.ts dist/redux-ts.min.js",
"build:test": "cross-env NODE_ENV=test webpack --output-filename lib/specs.js",
"clean": "rimraf dist lib",
"build": "npm run build:test && npm run build:prod && npm run build:prod:min && npm run build:dev",
"build": "npm run build:dev && npm run build:test && npm run build:prod && npm run build:prod:min",
"postinstall": "typings install",
"prepublish": "npm run clean && npm run build",
"test": "mocha ./lib/specs.js",
"test:watch": "npm test -- --watch"
"test": "npm run build:test && npm run test:run",
"test:run": "mocha ./lib/specs.js"
},
"tags": [
"react",
Expand Down
58 changes: 7 additions & 51 deletions src/utils/actionHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import 'ts-helpers'
import * as _ from 'lodash'


//http://www.bluewire-technologies.com/2015/redux-actions-for-typescript/
export interface IAction<T extends Redux.Action> {
prototype: T;
}

export function Action<T extends SyncAction>(actionClass: IAction<T>) {
actionClass.prototype.type = actionClass.toString().match(/\w+/g)[1];;
}

export abstract class SyncAction implements Redux.Action {
type: string;
constructor() {
Expand All @@ -17,11 +20,9 @@ export abstract class SyncAction implements Redux.Action {
export type NullableDispatch = Redux.Dispatch<any> | void;

export abstract class AsyncAction extends SyncAction implements Promise<NullableDispatch> {
resolve: (value?: Redux.Dispatch<any>) => void;
reject: (reason?: any) => void;
promise: Promise<Redux.Dispatch<any>> = new Promise<Redux.Dispatch<any>>((resolve, reject) => {
private resolve: (value?: Redux.Dispatch<any>) => void;
private promise: Promise<Redux.Dispatch<any>> = new Promise<Redux.Dispatch<any>>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});

then(onfulfilled?: (value: Redux.Dispatch<any>) => NullableDispatch | PromiseLike<NullableDispatch>, onrejected?: (reason: any) => void): Promise<NullableDispatch> {
Expand All @@ -33,53 +34,8 @@ export abstract class AsyncAction extends SyncAction implements Promise<Nullable
}
}

export function Action<T extends SyncAction>(actionClass: IAction<T>) {
actionClass.prototype.type = actionClass.toString().match(/\w+/g)[1];;
}


@Action
export class ShowLoading extends SyncAction { }


@Action
export class HideLoading extends SyncAction { }


const isAsyncAction = (action: AsyncAction | any): action is AsyncAction => {
return (<AsyncAction>action).promise !== undefined;
}

export const typedToPlainMiddleware: Redux.Middleware =
<S>(store: Redux.MiddlewareAPI<S>) => (next: Redux.Dispatch<S>): Redux.Dispatch<S> => (action: any) => {
if (typeof action === "object") {
action = _.merge({}, action);
}
return next(action);
};

export const asyncMiddleware: Redux.Middleware =
<S>(store: Redux.MiddlewareAPI<S>) => (next: Redux.Dispatch<S>): Redux.Dispatch<S> => (action: Redux.Action) => {
if (isAsyncAction(action)) {

//First dispatch show loading action synchronously
store.dispatch(new ShowLoading());

//Change state immediately and register async operations
var nextState = next(action);

//Lastly dispatch hide loading action asynchronously
action.then(dispatch => {
dispatch(new HideLoading());
});

//After original dispatch lifecycle, resolve dispatch in order to handle async operations
setTimeout(() => {
action.resolve(store.dispatch);
});

return nextState;
}

return next(action);
};
export class HideLoading extends SyncAction { }
32 changes: 32 additions & 0 deletions src/utils/asyncMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AsyncAction, ShowLoading, HideLoading } from './actionHelpers'


const isAsyncAction = (action: AsyncAction | any): action is AsyncAction => {
return action.resolve !== undefined && typeof action.resolve === "function";
}

export const asyncMiddleware = <S>(store: Redux.MiddlewareAPI<S>) => (next: Redux.Dispatch<S>): Redux.Dispatch<S> => (action: Redux.Action) => {
//Fix: Actions must be plain objects.
action = _.merge({}, action);

if (isAsyncAction(action)) {

//First dispatch show loading action synchronously
store.dispatch(new ShowLoading());

//Change state immediately and register async operations
var nextState = next(action);

//Lastly dispatch hide loading action asynchronously
action.then(dispatch => {
dispatch(new HideLoading());
});

//After original dispatch lifecycle, resolve dispatch in order to handle async operations
setTimeout(() => {
(<any>action).resolve(store.dispatch);
});
return nextState;
}
return next(action);
};
94 changes: 94 additions & 0 deletions src/utils/reducerBuilder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'mocha'
import { expect } from 'chai'
import { StoreBuilder } from './storeBuilder'
import { ReducerBuilder } from './reducerBuilder'
import { SyncAction, AsyncAction, Action, HideLoading, ShowLoading } from './actionHelpers'


interface SampleState {
isSet: boolean;
}

interface SampleStore {
reducer: SampleState
}

@Action
class SampleAction extends SyncAction {
}

@Action
class SampleAsyncAction extends AsyncAction {
}


describe("Reducer", () => {

describe("with inital state", () => {
var reducer = new ReducerBuilder<SampleState>()
.init({ isSet: true })
.build();

var store = new StoreBuilder<SampleStore>()
.withReducersMap({ reducer })
.build();

it("should have correct value", () => {
expect(store.getState().reducer.isSet).equal(true);
});
});

describe("with sync action handler", () => {
var reducer = new ReducerBuilder<SampleState>()
.handle(SampleAction, (state: SampleState, action: SampleAction) => {
state.isSet = true;
return state;
})
.build();

var store = new StoreBuilder<SampleStore>()
.withReducersMap({ reducer })
.build();

store.dispatch(new SampleAction());

it("should be called on dispatch action", () => {
expect(store.getState().reducer.isSet).equal(true);
});
});

describe("with async action handler", () => {
var dispatchedEvents: any[] = [];

var reducer: Redux.Reducer<any> = (state: any = {}, action: Redux.Action) => {
if (!action.type.startsWith("@@")) {
dispatchedEvents.push(action.type);
}
return state;
};

var store = new StoreBuilder<SampleStore>()
.withReducersMap({ reducer })
.build();

before(done => {
var action = new SampleAsyncAction();
store.dispatch(action);

action.then(dispatch => {
dispatch(new SampleAction());
done();
})
})

it("should be dispatched in correct order", () => {
expect(dispatchedEvents).deep.equal([
ShowLoading.prototype.type,
SampleAsyncAction.prototype.type,
HideLoading.prototype.type,
SampleAction.prototype.type
]);
});
});

});
7 changes: 3 additions & 4 deletions src/utils/reducerBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ export type Reducer<State, ActionType extends SyncAction> = (state: State, actio

export class ReducerBuilder<State> {

actions: { [type: string]: Reducer<State, SyncAction> } = {};
initState: State;

private actions: { [type: string]: Reducer<State, SyncAction> } = {};
private initState: State;

public init(state: State) {
this.initState = state;
Expand All @@ -32,7 +31,7 @@ export class ReducerBuilder<State> {
return _.merge({}, state, nextState);
}

return state;
return state || {};
}
}
}
86 changes: 76 additions & 10 deletions src/utils/storeBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,88 @@ import { expect } from 'chai'
import { StoreBuilder } from './storeBuilder'


describe("StoreBuilder", () => {
describe("Store", () => {

var store: Redux.Store<{}>;
var INITIAL_STATE = { test: true }
var enhancer = (f: Redux.StoreCreator) => { return f; };
var TestAction = <Redux.Action>{ type: "test" };

describe("with inital state", () => {
var state = { test: true }
var store = new StoreBuilder()
.withInitialState(state)
.build();

beforeEach(() => {
store = new StoreBuilder()
.withInitialState(INITIAL_STATE)
.withEnhancer(enhancer)
it("should have correct value", () => {
expect(store.getState()).equal(state);
});
});


describe("with middleware", () => {
var isSet = false;
var testMiddleware = (store: any) => (next: any) => (action: any) => { isSet = true; }
var store = new StoreBuilder()
.withMiddleware(testMiddleware)
.build();

store.dispatch(TestAction);

it("should be called on dispatch action", () => {
expect(isSet).equal(true);
});
});


describe("with reducer", () => {
var isSet = false;
var testReducer = (state = {}, action: Redux.Action) => {
if (action.type == TestAction.type) { isSet = true; }
return state;
};
var store = new StoreBuilder()
.withReducer("test", testReducer)
.build();

store.dispatch(TestAction);

it("should be called on dispatch action", () => {
expect(isSet).equal(true);
});
});


describe("with reducer map", () => {
var isSet = false;
var testReducer = (state = {}, action: Redux.Action) => {
if (action.type == TestAction.type) { isSet = true; }
return state;
}
var store = new StoreBuilder()
.withReducersMap({ testReducer })
.build();

store.dispatch(TestAction);

it("should be called on dispatch action", () => {
expect(isSet).equal(true);
});
});

it("should have initial state", () => {
expect(store.getState()).equal(INITIAL_STATE);

describe("with enhancer", () => {
var isSet = false;
var enhancer = (f: Redux.StoreCreator) => {
isSet = true;
return f;
};
var store = new StoreBuilder()
.withEnhancer(enhancer)
.build();

store.dispatch(TestAction);

it("should be called on dispatch action", () => {
expect(isSet).equal(true);
});
});

});
7 changes: 3 additions & 4 deletions src/utils/storeBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as _ from 'lodash'
import { combineReducers, createStore, applyMiddleware, compose } from 'redux';
import { typedToPlainMiddleware, asyncMiddleware } from '../utils/actionHelpers'
import { asyncMiddleware } from '../utils/asyncMiddleware'


export class StoreBuilder<StoreType> {
Expand All @@ -11,7 +11,7 @@ export class StoreBuilder<StoreType> {
private enhancer: Redux.GenericStoreEnhancer;

constructor() {
this.middlewares = [typedToPlainMiddleware, asyncMiddleware];
this.middlewares = [asyncMiddleware];
this.reducers = {};
this.initialState = {} as StoreType;
this.enhancer = (f: Redux.StoreCreator) => f;
Expand All @@ -33,7 +33,7 @@ export class StoreBuilder<StoreType> {
}

public withReducersMap(reducers: Redux.ReducersMapObject) {
this.reducers = _.merge({}, this.reducers, reducers);
this.reducers = _.merge({}, this.reducers, reducers) as Redux.ReducersMapObject;
return this;
}

Expand All @@ -45,7 +45,6 @@ export class StoreBuilder<StoreType> {


public build() {

let middlewares = applyMiddleware(...this.middlewares);
let reducers = combineReducers(this.reducers);
let composer = compose(middlewares, this.enhancer)(createStore);
Expand Down
Loading

0 comments on commit ef8453c

Please sign in to comment.