Skip to content

Commit

Permalink
test(createEffects): Coverage >95%
Browse files Browse the repository at this point in the history
  • Loading branch information
isierra committed Oct 18, 2016
1 parent 3cb427e commit a4b02aa
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 49 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"rxjs": "^5.0.0-rc.1"
},
"jest": {
"verbose": true,
"scriptPreprocessor": "node_modules/typescript-babel-jest",
"testEnvironment": "node",
"testRegex": ".*\\.spec\\.ts$",
Expand All @@ -74,4 +75,4 @@
"json"
]
}
}
}
186 changes: 186 additions & 0 deletions src/createEffects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/// <reference path="../typefix/jest.d.ts" />
"use strict";

import "jest";
require("babel-core/register");
require("babel-polyfill");
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/of";
import "rxjs/add/observable/empty";
import "rxjs/add/operator/bufferTime";
import "rxjs/add/operator/filter";
import "rxjs/add/operator/first";
import "rxjs/add/operator/map";
import "rxjs/add/operator/take";
import "rxjs/add/operator/toArray";
import "rxjs/add/operator/toPromise";

import {
Store, Action, Reducer, StateUpdate, EffectsDisposer,
} from "./interfaces";
import { createStore, createStoreExtensions } from "./createStore";
import {
logUpdatesEffect, logUpdatesByActionTypeEffect, consoleLogUpdatesEffect,
createEffects, createDisposers,
} from "./createEffects";

describe("logUpdatesEffect", () => {
it("it should be a function",
() => expect(typeof logUpdatesEffect).toBe("function"));
describe("When it is called with a captioner", () => {
const captioner = jest.fn(() => "***CAPTION***");
const captionedEffect = logUpdatesEffect(captioner);
it("The result should be a function",
() => expect(typeof captionedEffect).toBe("function"));
it("The captioner should not be called",
() => expect(captioner).not.toBeCalled());
describe("When it is called with a logger", () => {
const logger = jest.fn();
const loggerEffect = captionedEffect(logger);
it("The result should be a function",
() => expect(typeof loggerEffect).toBe("function"));
it("The captioner should not be called",
() => expect(captioner).not.toBeCalled());
it("The logger should not be called",
() => expect(logger).not.toBeCalled());
describe("When a store gets the effect applied", () => {
const reducer = jest.fn((s, a) => s);
const state = { title: "hello" };
const store = createStore(reducer, state);
const update = {
action: { type: "A" },
state,
previousState: state,
};
const loggerPromise = loggerEffect(store).take(1).first().toPromise() as PromiseLike<StateUpdate<{ title: string }>>;
it("captioner and logger should have been called once",
() => {
store.dispatch(update.action);
expect(captioner).toHaveBeenCalledTimes(1);
expect(logger).toHaveBeenCalledTimes(1);
expect(captioner).toBeCalledWith(update, store);
expect(logger).toBeCalledWith("***CAPTION***", update);
return loggerPromise.then(up => expect(up).toEqual(update));
});

}); // describe When a store gets the effect applied
}); // describe When it is called with a logger
}); // describe When it is called with a captioner
}); // describe logUpdatesEffect

describe("logUpdatesByActionTypeEffect", () => {
describe("When it is called with a logger", () => {
const logger = jest.fn();
const loggerEffect = logUpdatesByActionTypeEffect(logger);
it("The result should be a function",
() => expect(typeof loggerEffect).toBe("function"));
it("The logger should not be called",
() => expect(logger).not.toBeCalled());
describe("When a store gets the effect applied", () => {
const reducer = jest.fn((s, a) => s);
const state = { title: "hello" };
const store = createStore(reducer, state, {
extendWith: () => ({ caption: "MyStore" }),
});
const update = {
action: { type: "A" },
state,
previousState: state,
};
const loggerPromise = loggerEffect(store).take(1).first().toPromise() as PromiseLike<StateUpdate<{ title: string }>>;
it("logger should have been called once",
() => {
store.dispatch(update.action);
expect(logger).toHaveBeenCalledTimes(1);
expect(logger).toBeCalledWith("MyStore: ON A", update);
return loggerPromise.then(up => {
expect(up).toEqual(update);
});
});
}); // describe When a store gets the effect applied
}); // describe When it is called with a logger
}); // describe logUpdatesByActionTypeEffect

describe("createDisposers", () => {
describe("Given no disposers", () => {
describe("When createDisposers is called without disposers", () => {
const effects = createDisposers();
it("it should be able to be called without any visible effects",
() => effects());
}); // describe When createDisposers is called without disposers
}); // describe Given no disposers

describe("Given one disposer", () => {
const disp1 = jest.fn();
describe("When createDisposers is called with one disposer", () => {
const effects = createDisposers(disp1);
it("it should not call the disposer before dispose is called",
() => expect(disp1).not.toBeCalled());
it("it should call the disposer after dispose has been called",
() => {
effects();
expect(disp1).toHaveBeenCalledTimes(1);
});
it("it should not call the disposer a second time",
() => {
effects();
expect(disp1).toHaveBeenCalledTimes(1);
});
}); // describe When createDisposers is called without disposers
}); // describe Given no disposers
}); // describe createDisposers

describe("createEffects", () => {
describe("Given a simple store", () => {
const reducer = jest.fn((s, a) => s);
const state = 0;
const store = createStore(reducer, state);
describe("Given some PONG effect", () => {
const pong = store.action$
.filter(a => a.type === "PING")
.map(() => ({ type: "PONG" }));
describe("When effects are associated with the store", () => {
const actionsPromise = store.action$.take(2).toArray().toPromise() as PromiseLike<Action[]>;
const effects = createEffects(store.dispatch, pong);
it("a PONG action should be emited for a PING",
() => {
store.dispatch({ type: "PING" });
return actionsPromise.then(actions => {
expect(actions).toEqual([
{ type: "PING" },
{ type: "PONG" },
]);
});
});

}); // describe When efects are associated with the store
}); // describe Given some PING/PONG effect
}); // describe Given a simple store

// describe("Given a simple store", () => {
// const reducer = jest.fn((s, a) => s);
// const state = 0;
// const store = createStore(reducer, state);
// describe("Given some PONG effect", () => {
// // const pong = store.action$
// // .filter(a => a.type === "PING")
// // .map(() => ({ type: "PONG" }));
// describe("When effects are associated with the store and disposed", () => {
// const actionsPromise = store.action$.bufferTime(100).toPromise() as PromiseLike<Action[]>;
// // const effects = createEffects(store.dispatch, pong);
// // effects();
// it("a PONG action should not be emited for a PING",
// () => {
// store.dispatch({ type: "PING" });
// return actionsPromise.then(actions => {
// expect(actions).toEqual([
// { type: "PING" },
// // { type: "PONG" },
// ]);
// });
// });

// }); // describe When efects are associated with the store
// }); // describe Given some PING/PONG effect
// }); // describe Given a simple store
}); // describe createEffects
56 changes: 35 additions & 21 deletions src/createEffects.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,55 @@
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/delay";
import "rxjs/add/operator/do";
import "rxjs/add/operator/subscribeOn";
import { queue } from "rxjs/scheduler/queue";
import { Dispatcher, Effect, EffectsDisposer } from "./interfaces";
import {
Action, Dispatcher, Effect, EffectsDisposer, Store, StateUpdate,
} from "./interfaces";

const scheduler = queue;

export const EFFECTS_ACTIONS = {
END_STORE_EFFECTS: "END_STORE_EFFECTS",
INIT_STORE_EFFECTS: "INIT_STORE_EFFECTS",
};
export const logUpdatesEffect =
(captioner: (update: StateUpdate<any>, store: Store<any>) => any) =>
(logger: (message?: any, ...parameters: any[]) => void) =>
(store: Store<any>) =>
store.update$
.do((up: StateUpdate<any>) => logger(captioner(up, store), up));

// export const logUpdatesEffect = (store: Store<any>) => store.update$
// .do(up => console.log("UPDATE: " + up.action.type, up));
export const logUpdatesByActionTypeEffect =
logUpdatesEffect((up, store) => {
let caption = (store as any).caption;
if (caption) { caption += ": "; } else { caption = ""; }
return caption + "ON " + up.action.type;
});

export const createEffects =
(dispatch: Dispatcher, ...effects: (Effect | EffectsDisposer)[])
export const consoleLogUpdatesEffect =
logUpdatesByActionTypeEffect(console.log.bind(console));

export const createDisposers =
(...disposers: EffectsDisposer[])
: EffectsDisposer => {
let disposers = effects
.map(e => {
if (typeof e === "function") {
return e;
} else {
const subscription = e.subscribeOn(scheduler).subscribe(dispatch);
return () => subscription.unsubscribe();
}
});
let isDisposed = false;

const unlisten = () => {
if (isDisposed) { return; }
isDisposed = true;
dispatch({ type: EFFECTS_ACTIONS.END_STORE_EFFECTS });
disposers.forEach(s => s());
disposers = [];
};

dispatch({ type: EFFECTS_ACTIONS.INIT_STORE_EFFECTS });
return unlisten;
};

export const createEffects =
(dispatch: Dispatcher, ...effects: Effect[])
: EffectsDisposer => {
const toDisposer = (e: Effect) => {
const subscription = e.subscribeOn(scheduler).subscribe(dispatch);
return () => subscription.unsubscribe();
};
let disposers = effects.map(toDisposer);

return createDisposers(...disposers);
};

export default createEffects;
2 changes: 2 additions & 0 deletions src/createReducerFromMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export const createReducerFromMap =
}
return reducer as TState;
};

export default createReducerFromMap;
9 changes: 6 additions & 3 deletions src/createStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import "rxjs/add/operator/toArray";
import "rxjs/add/operator/toPromise";

import {
Store, Action, Reducer, StateUpdate, StoreActionsMap,
Action, StateUpdate,
} from "./interfaces";
import { createStore, createStoreExtensions } from "./createStore";

Expand Down Expand Up @@ -124,9 +124,12 @@ describe("createStore", () => {
describe("When an action is dispatched in the store through an extension", () => {
const reducer = jest.fn((s, a) => ({ title: s.title + a.payload }));
const state = { title: "hello" };
const store = createStore(reducer, state, createStoreExtensions({
const extendWith = createStoreExtensions({
concat: (str: string) => ({ type: "CONCAT", payload: str }),
}));
});
const store = createStore(reducer, state, {
extendWith,
});
const action = { type: "CONCAT", payload: " world" };
(store as any).concat(" world");
store.dispatch(action);
Expand Down
7 changes: 6 additions & 1 deletion src/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,17 @@ export const createStoreExtensions =
return result;
};

export interface CreateStoreOptions<TState, TStore extends Store<TState>> {
extendWith?: (store: Store<TState>) => Object;
}

export const createStore =
<TState, TStore extends Store<TState>>(
reducer: Reducer<TState>,
initialState: TState,
extendWith?: (store: Store<TState>) => Object
options?: CreateStoreOptions<TState, TStore>
): TStore => {
const { extendWith = undefined } = options || {};
const stateSubject$ = new BehaviorSubject<TState>(initialState);
const actionSubject$ = new Subject<Action>();
const updateSubject$ = new Subject<StateUpdate<TState>>();
Expand Down
9 changes: 7 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"lib": ["es6", "dom"],
"lib": [
"es6",
"dom"
],
"module": "commonjs",
"target": "es6",
"noImplicitAny": true,
Expand All @@ -12,6 +15,7 @@
"declaration": true,
"outDir": "dist",
"removeComments": false,
"strictNullChecks": true,
"typeRoots": [
"node_modules/@types"
],
Expand All @@ -20,9 +24,10 @@
]
},
"files": [
"typefix/index.d.ts",
"src/main.ts"
],
"exclude": [
"node_modules"
]
}
}
Loading

0 comments on commit a4b02aa

Please sign in to comment.