From 4b62d964a9df26aa060d55c13873be51c1ae1794 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 11 Jan 2017 09:40:23 +0100 Subject: [PATCH 1/6] added tests for headerReducer and HeaderActions --- package.json | 2 + src/app/header/header.actions.spec.ts | 22 ++++++++ src/app/header/header.reducer.spec.ts | 76 +++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/app/header/header.actions.spec.ts create mode 100644 src/app/header/header.reducer.spec.ts diff --git a/package.json b/package.json index 44316b310e..d72b0e2f02 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@types/body-parser": "0.0.33", "@types/compression": "0.0.33", "@types/cookie-parser": "1.3.30", + "@types/deep-freeze": "0.0.29", "@types/express": "4.0.34", "@types/express-serve-static-core": "4.0.39", "@types/hammerjs": "2.0.33", @@ -123,6 +124,7 @@ "cookie-parser": "1.4.3", "copy-webpack-plugin": "4.0.1", "css-loader": "^0.26.0", + "deep-freeze": "0.0.1", "html-webpack-plugin": "^2.21.0", "imports-loader": "0.7.0", "istanbul-instrumenter-loader": "^0.2.0", diff --git a/src/app/header/header.actions.spec.ts b/src/app/header/header.actions.spec.ts new file mode 100644 index 0000000000..8ea2094ee4 --- /dev/null +++ b/src/app/header/header.actions.spec.ts @@ -0,0 +1,22 @@ +import { HeaderActions } from "./header.actions"; +describe("HeaderActions", () => { + + describe("collapse", () => { + it("should return a COLLAPSE action", () => { + expect(HeaderActions.collapse().type).toEqual(HeaderActions.COLLAPSE); + }); + }); + + describe("expand", () => { + it("should return an EXPAND action", () => { + expect(HeaderActions.expand().type).toEqual(HeaderActions.EXPAND); + }); + }); + + describe("toggle", () => { + it("should return a TOGGLE action", () => { + expect(HeaderActions.toggle().type).toEqual(HeaderActions.TOGGLE); + }); + }) + +}); diff --git a/src/app/header/header.reducer.spec.ts b/src/app/header/header.reducer.spec.ts new file mode 100644 index 0000000000..03efcf1359 --- /dev/null +++ b/src/app/header/header.reducer.spec.ts @@ -0,0 +1,76 @@ +import * as deepFreeze from "deep-freeze"; + +import { headerReducer } from "./header.reducer"; +import { HeaderActions } from "./header.actions"; + +describe("headerReducer", () => { + + it("should return the current state when no valid actions have been made", () => { + const state = { navCollapsed: false }; + const newState = headerReducer(state, {type: 'undefined-action'}); + + expect(newState).toEqual(state); + }); + + it("should start with navCollapsed = true", () => { + const initialState = headerReducer(undefined, {type: 'undefined-action'}); + + // The navigation starts collapsed + expect(initialState.navCollapsed).toEqual(true); + }); + + it("should set navCollapsed to true in response to the COLLAPSE action", () => { + const state = { navCollapsed: false }; + const action = HeaderActions.collapse(); + const newState = headerReducer(state, action); + + expect(newState.navCollapsed).toEqual(true); + }); + + it("should perform the COLLAPSE action without mutating the previous state", () => { + const state = { navCollapsed: false }; + deepFreeze(state); + + const action = HeaderActions.collapse(); + headerReducer(state, action); + + //no expect required, deepFreeze will ensure an exception is thrown if the state + //is mutated, and any uncaught exception will cause the test to fail + }); + + it("should set navCollapsed to false in response to the EXPAND action", () => { + const state = { navCollapsed: true }; + const action = HeaderActions.expand(); + const newState = headerReducer(state, action); + + expect(newState.navCollapsed).toEqual(false); + }); + + it("should perform the EXPAND action without mutating the previous state", () => { + const state = { navCollapsed: true }; + deepFreeze(state); + + const action = HeaderActions.expand(); + headerReducer(state, action); + }); + + it("should flip the value of navCollapsed in response to the TOGGLE action", () => { + const state1 = { navCollapsed: true }; + const action = HeaderActions.toggle(); + + const state2 = headerReducer(state1, action); + const state3 = headerReducer(state2, action); + + expect(state2.navCollapsed).toEqual(false); + expect(state3.navCollapsed).toEqual(true); + }); + + it("should perform the TOGGLE action without mutating the previous state", () => { + const state = { navCollapsed: true }; + deepFreeze(state); + + const action = HeaderActions.toggle(); + headerReducer(state, action); + }); + +}); From 8e4bec9c304f7f1b0bfcebdbde96b7c6fe35c2b9 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 13 Jan 2017 11:25:07 +0100 Subject: [PATCH 2/6] added tests for hostReducer and Header Effects, and switch to typed actions --- .gitignore | 1 + src/app/app.component.ts | 4 +- src/app/header/header.actions.spec.ts | 22 --------- src/app/header/header.actions.ts | 57 ++++++++++++++-------- src/app/header/header.component.ts | 13 +++-- src/app/header/header.effects.spec.ts | 53 ++++++++++++++++++++ src/app/header/header.effects.ts | 10 ++-- src/app/header/header.reducer.spec.ts | 24 +++++---- src/app/header/header.reducer.ts | 11 ++--- src/app/shared/host-window.actions.ts | 27 ++++++---- src/app/shared/host-window.reducer.spec.ts | 40 +++++++++++++++ src/app/shared/host-window.reducer.ts | 7 ++- src/app/shared/ngrx/type.ts | 24 +++++++++ 13 files changed, 211 insertions(+), 82 deletions(-) delete mode 100644 src/app/header/header.actions.spec.ts create mode 100644 src/app/header/header.effects.spec.ts create mode 100644 src/app/shared/host-window.reducer.spec.ts create mode 100644 src/app/shared/ngrx/type.ts diff --git a/.gitignore b/.gitignore index 0e713600f8..f691aae4b2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ npm-debug.log /dist/ +/coverage/ .idea *.ngfactory.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 15485eb6a6..0b861bb75c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -8,7 +8,7 @@ import { import { TranslateService } from "ng2-translate"; import { HostWindowState } from "./shared/host-window.reducer"; import { Store } from "@ngrx/store"; -import { HostWindowActions } from "./shared/host-window.actions"; +import { HostWindowResizeAction } from "./shared/host-window.actions"; @Component({ changeDetection: ChangeDetectionStrategy.Default, @@ -52,7 +52,7 @@ export class AppComponent implements OnDestroy, OnInit { @HostListener('window:resize', ['$event']) private onResize(event): void { this.store.dispatch( - HostWindowActions.resize(event.target.innerWidth, event.target.innerHeight) + new HostWindowResizeAction(event.target.innerWidth, event.target.innerHeight) ); } diff --git a/src/app/header/header.actions.spec.ts b/src/app/header/header.actions.spec.ts deleted file mode 100644 index 8ea2094ee4..0000000000 --- a/src/app/header/header.actions.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { HeaderActions } from "./header.actions"; -describe("HeaderActions", () => { - - describe("collapse", () => { - it("should return a COLLAPSE action", () => { - expect(HeaderActions.collapse().type).toEqual(HeaderActions.COLLAPSE); - }); - }); - - describe("expand", () => { - it("should return an EXPAND action", () => { - expect(HeaderActions.expand().type).toEqual(HeaderActions.EXPAND); - }); - }); - - describe("toggle", () => { - it("should return a TOGGLE action", () => { - expect(HeaderActions.toggle().type).toEqual(HeaderActions.TOGGLE); - }); - }) - -}); diff --git a/src/app/header/header.actions.ts b/src/app/header/header.actions.ts index e3f64c1019..c333fc5df7 100644 --- a/src/app/header/header.actions.ts +++ b/src/app/header/header.actions.ts @@ -1,24 +1,43 @@ import { Action } from "@ngrx/store"; +import { type } from "../shared/ngrx/type"; -export class HeaderActions { - static COLLAPSE = 'dspace/header/COLLAPSE'; - static collapse(): Action { - return { - type: HeaderActions.COLLAPSE - } - } + /** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const HeaderActionTypes = { + COLLAPSE: type('dspace/header/COLLAPSE'), + EXPAND: type('dspace/header/EXPAND'), + TOGGLE: type('dspace/header/TOGGLE') +}; - static EXPAND = 'dspace/header/EXPAND'; - static expand(): Action { - return { - type: HeaderActions.EXPAND - } - } +export class HeaderCollapseAction implements Action { + type = HeaderActionTypes.COLLAPSE; - static TOGGLE = 'dspace/header/TOGGLE'; - static toggle(): Action { - return { - type: HeaderActions.TOGGLE - } - } + constructor() {} } + +export class HeaderExpandAction implements Action { + type = HeaderActionTypes.EXPAND; + + constructor() {} +} + +export class HeaderToggleAction implements Action { + type = HeaderActionTypes.TOGGLE; + + constructor() {} +} + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types + */ +export type HeaderAction + = HeaderCollapseAction + | HeaderExpandAction + | HeaderToggleAction diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 69da1aea68..29cac9cadd 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -1,9 +1,12 @@ import { Component, OnInit } from "@angular/core"; import { Store } from "@ngrx/store"; import { HeaderState } from "./header.reducer"; -import { HeaderActions } from "./header.actions"; import { Observable } from "rxjs"; -import 'rxjs/add/operator/filter'; +import { + HeaderCollapseAction, + HeaderExpandAction, + HeaderToggleAction +} from "./header.actions"; @Component({ selector: 'ds-header', @@ -25,15 +28,15 @@ export class HeaderComponent implements OnInit { } private collapse(): void { - this.store.dispatch(HeaderActions.collapse()); + this.store.dispatch(new HeaderCollapseAction()); } private expand(): void { - this.store.dispatch(HeaderActions.expand()); + this.store.dispatch(new HeaderExpandAction()); } public toggle(): void { - this.store.dispatch(HeaderActions.toggle()); + this.store.dispatch(new HeaderToggleAction()); } } diff --git a/src/app/header/header.effects.spec.ts b/src/app/header/header.effects.spec.ts new file mode 100644 index 0000000000..7c5f40228d --- /dev/null +++ b/src/app/header/header.effects.spec.ts @@ -0,0 +1,53 @@ +import { TestBed, inject } from "@angular/core/testing"; +import { EffectsTestingModule, EffectsRunner } from '@ngrx/effects/testing'; +import { HeaderEffects } from "./header.effects"; +import { HeaderCollapseAction } from "./header.actions"; +import { HostWindowResizeAction } from "../shared/host-window.actions"; +import { routerActions } from "@ngrx/router-store"; + +describe('HeaderEffects', () => { + beforeEach(() => TestBed.configureTestingModule({ + imports: [ + EffectsTestingModule + ], + providers: [ + HeaderEffects + ] + })); + + let runner: EffectsRunner; + let headerEffects: HeaderEffects; + + beforeEach(inject([ + EffectsRunner, HeaderEffects + ], + (_runner, _headerEffects) => { + runner = _runner; + headerEffects = _headerEffects; + } + )); + + describe('resize$', () => { + + it('should return a COLLAPSE action in response to a RESIZE action', () => { + runner.queue(new HostWindowResizeAction(800,600)); + + headerEffects.resize$.subscribe(result => { + expect(result).toEqual(new HeaderCollapseAction()); + }); + }); + + }); + + describe('routeChange$', () => { + + it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => { + runner.queue({ type: routerActions.UPDATE_LOCATION }); + + headerEffects.resize$.subscribe(result => { + expect(result).toEqual(new HeaderCollapseAction()); + }); + }); + + }); +}); diff --git a/src/app/header/header.effects.ts b/src/app/header/header.effects.ts index d9ea53adda..33888e074d 100644 --- a/src/app/header/header.effects.ts +++ b/src/app/header/header.effects.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; import { Effect, Actions } from '@ngrx/effects' -import { HeaderActions } from "./header.actions"; -import { HostWindowActions } from "../shared/host-window.actions"; +import { HostWindowActionTypes } from "../shared/host-window.actions"; import { routerActions } from "@ngrx/router-store"; +import { HeaderCollapseAction } from "./header.actions"; @Injectable() export class HeaderEffects { @@ -12,10 +12,10 @@ export class HeaderEffects { ) { } @Effect() resize$ = this.actions$ - .ofType(HostWindowActions.RESIZE) - .map(() => HeaderActions.collapse()); + .ofType(HostWindowActionTypes.RESIZE) + .map(() => new HeaderCollapseAction()); @Effect() routeChange$ = this.actions$ .ofType(routerActions.UPDATE_LOCATION) - .map(() => HeaderActions.collapse()); + .map(() => new HeaderCollapseAction()); } diff --git a/src/app/header/header.reducer.spec.ts b/src/app/header/header.reducer.spec.ts index 03efcf1359..a3f9f50021 100644 --- a/src/app/header/header.reducer.spec.ts +++ b/src/app/header/header.reducer.spec.ts @@ -1,19 +1,25 @@ import * as deepFreeze from "deep-freeze"; import { headerReducer } from "./header.reducer"; -import { HeaderActions } from "./header.actions"; +import { + HeaderCollapseAction, + HeaderExpandAction, + HeaderToggleAction +} from "./header.actions"; describe("headerReducer", () => { + let nullAction = new HeaderCollapseAction(); + nullAction.type = null; it("should return the current state when no valid actions have been made", () => { const state = { navCollapsed: false }; - const newState = headerReducer(state, {type: 'undefined-action'}); + const newState = headerReducer(state, nullAction); expect(newState).toEqual(state); }); it("should start with navCollapsed = true", () => { - const initialState = headerReducer(undefined, {type: 'undefined-action'}); + const initialState = headerReducer(undefined, nullAction); // The navigation starts collapsed expect(initialState.navCollapsed).toEqual(true); @@ -21,7 +27,7 @@ describe("headerReducer", () => { it("should set navCollapsed to true in response to the COLLAPSE action", () => { const state = { navCollapsed: false }; - const action = HeaderActions.collapse(); + const action = new HeaderCollapseAction(); const newState = headerReducer(state, action); expect(newState.navCollapsed).toEqual(true); @@ -31,7 +37,7 @@ describe("headerReducer", () => { const state = { navCollapsed: false }; deepFreeze(state); - const action = HeaderActions.collapse(); + const action = new HeaderCollapseAction(); headerReducer(state, action); //no expect required, deepFreeze will ensure an exception is thrown if the state @@ -40,7 +46,7 @@ describe("headerReducer", () => { it("should set navCollapsed to false in response to the EXPAND action", () => { const state = { navCollapsed: true }; - const action = HeaderActions.expand(); + const action = new HeaderExpandAction(); const newState = headerReducer(state, action); expect(newState.navCollapsed).toEqual(false); @@ -50,13 +56,13 @@ describe("headerReducer", () => { const state = { navCollapsed: true }; deepFreeze(state); - const action = HeaderActions.expand(); + const action = new HeaderExpandAction(); headerReducer(state, action); }); it("should flip the value of navCollapsed in response to the TOGGLE action", () => { const state1 = { navCollapsed: true }; - const action = HeaderActions.toggle(); + const action = new HeaderToggleAction(); const state2 = headerReducer(state1, action); const state3 = headerReducer(state2, action); @@ -69,7 +75,7 @@ describe("headerReducer", () => { const state = { navCollapsed: true }; deepFreeze(state); - const action = HeaderActions.toggle(); + const action = new HeaderToggleAction(); headerReducer(state, action); }); diff --git a/src/app/header/header.reducer.ts b/src/app/header/header.reducer.ts index af153fba24..c1d0fef7ea 100644 --- a/src/app/header/header.reducer.ts +++ b/src/app/header/header.reducer.ts @@ -1,5 +1,4 @@ -import { Action } from "@ngrx/store"; -import { HeaderActions } from "./header.actions"; +import { HeaderAction, HeaderActionTypes } from "./header.actions"; export interface HeaderState { navCollapsed: boolean; @@ -9,23 +8,23 @@ const initialState: HeaderState = { navCollapsed: true }; -export const headerReducer = (state = initialState, action: Action): HeaderState => { +export const headerReducer = (state = initialState, action: HeaderAction): HeaderState => { switch (action.type) { - case HeaderActions.COLLAPSE: { + case HeaderActionTypes.COLLAPSE: { return Object.assign({}, state, { navCollapsed: true }); } - case HeaderActions.EXPAND: { + case HeaderActionTypes.EXPAND: { return Object.assign({}, state, { navCollapsed: false }); } - case HeaderActions.TOGGLE: { + case HeaderActionTypes.TOGGLE: { return Object.assign({}, state, { navCollapsed: !state.navCollapsed }); diff --git a/src/app/shared/host-window.actions.ts b/src/app/shared/host-window.actions.ts index de41c69564..99be01cdc2 100644 --- a/src/app/shared/host-window.actions.ts +++ b/src/app/shared/host-window.actions.ts @@ -1,14 +1,21 @@ import { Action } from "@ngrx/store"; +import { type } from "./ngrx/type"; -export class HostWindowActions { - static RESIZE = 'dspace/host-window/RESIZE'; - static resize(newWidth: number, newHeight: number): Action { - return { - type: HostWindowActions.RESIZE, - payload: { - width: newWidth, - height: newHeight - } - } +export const HostWindowActionTypes = { + RESIZE: type('dspace/host-window/RESIZE') +}; + +export class HostWindowResizeAction implements Action { + type = HostWindowActionTypes.RESIZE; + payload: { + width: number; + height: number; + }; + + constructor(width: number, height: number) { + this.payload = { width, height } } } + +export type HostWindowAction + = HostWindowResizeAction; diff --git a/src/app/shared/host-window.reducer.spec.ts b/src/app/shared/host-window.reducer.spec.ts new file mode 100644 index 0000000000..d0c2e697c0 --- /dev/null +++ b/src/app/shared/host-window.reducer.spec.ts @@ -0,0 +1,40 @@ +import * as deepFreeze from "deep-freeze"; +import { hostWindowReducer } from "./host-window.reducer"; +import { HostWindowResizeAction } from "./host-window.actions"; + +describe('hostWindowReducer', () => { + let nullAction = new HostWindowResizeAction(0, 0); + nullAction.type = null; + + it("should return the current state when no valid actions have been made", () => { + const state = { width: 800, height: 600 }; + const newState = hostWindowReducer(state, nullAction); + + expect(newState).toEqual(state); + }); + + it("should start with width = null and height = null", () => { + const initialState = hostWindowReducer(undefined, nullAction); + + expect(initialState.width).toEqual(null); + expect(initialState.height).toEqual(null); + }); + + it("should update the width and height in the state in response to a RESIZE action", () => { + const state = { width: 800, height: 600 }; + const action = new HostWindowResizeAction(1024, 768); + const newState = hostWindowReducer(state, action); + + expect(newState.width).toEqual(1024); + expect(newState.height).toEqual(768); + }); + + it("should perform the RESIZE action without mutating the previous state", () => { + const state = { width: 800, height: 600 }; + deepFreeze(state); + + const action = new HostWindowResizeAction(1024, 768); + hostWindowReducer(state, action); + }); + +}); diff --git a/src/app/shared/host-window.reducer.ts b/src/app/shared/host-window.reducer.ts index 4b8e1d3cb9..03349b3a91 100644 --- a/src/app/shared/host-window.reducer.ts +++ b/src/app/shared/host-window.reducer.ts @@ -1,5 +1,4 @@ -import { Action } from "@ngrx/store"; -import { HostWindowActions } from "./host-window.actions"; +import { HostWindowAction, HostWindowActionTypes } from "./host-window.actions"; export interface HostWindowState { width: number; @@ -11,10 +10,10 @@ const initialState: HostWindowState = { height: null }; -export const hostWindowReducer = (state = initialState, action: Action): HostWindowState => { +export const hostWindowReducer = (state = initialState, action: HostWindowAction): HostWindowState => { switch (action.type) { - case HostWindowActions.RESIZE: { + case HostWindowActionTypes.RESIZE: { return Object.assign({}, state, action.payload); } diff --git a/src/app/shared/ngrx/type.ts b/src/app/shared/ngrx/type.ts new file mode 100644 index 0000000000..9b50c1d6d0 --- /dev/null +++ b/src/app/shared/ngrx/type.ts @@ -0,0 +1,24 @@ + +/** + * Based on + * https://github.com/ngrx/example-app/blob/master/src/app/util.ts + * + * This function coerces a string into a string literal type. + * Using tagged union types in TypeScript 2.0, this enables + * powerful typechecking of our reducers. + * + * Since every action label passes through this function it + * is a good place to ensure all of our action labels + * are unique. + */ + +let typeCache: { [label: string]: boolean } = {}; +export function type(label: T | ''): T { + if (typeCache[label]) { + throw new Error(`Action type "${label}" is not unique"`); + } + + typeCache[label] = true; + + return label; +} From 562f09a36656ad69e5e913309d995ea5ac02b4cc Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 17 Jan 2017 12:06:32 +0100 Subject: [PATCH 3/6] turned null actions in to classes --- src/app/header/header.reducer.spec.ts | 23 ++++++++++++++-------- src/app/shared/host-window.reducer.spec.ts | 18 ++++++++++++----- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/app/header/header.reducer.spec.ts b/src/app/header/header.reducer.spec.ts index a3f9f50021..2b8c26f523 100644 --- a/src/app/header/header.reducer.spec.ts +++ b/src/app/header/header.reducer.spec.ts @@ -7,19 +7,26 @@ import { HeaderToggleAction } from "./header.actions"; -describe("headerReducer", () => { - let nullAction = new HeaderCollapseAction(); - nullAction.type = null; +class NullAction extends HeaderCollapseAction { + type = null; + constructor() { + super(); + } +} + +describe("headerReducer", () => { it("should return the current state when no valid actions have been made", () => { const state = { navCollapsed: false }; - const newState = headerReducer(state, nullAction); + const action = new NullAction(); + const newState = headerReducer(state, action); expect(newState).toEqual(state); }); it("should start with navCollapsed = true", () => { - const initialState = headerReducer(undefined, nullAction); + const action = new NullAction(); + const initialState = headerReducer(undefined, action); // The navigation starts collapsed expect(initialState.navCollapsed).toEqual(true); @@ -33,7 +40,7 @@ describe("headerReducer", () => { expect(newState.navCollapsed).toEqual(true); }); - it("should perform the COLLAPSE action without mutating the previous state", () => { + it("should perform the COLLAPSE action without affecting the previous state", () => { const state = { navCollapsed: false }; deepFreeze(state); @@ -52,7 +59,7 @@ describe("headerReducer", () => { expect(newState.navCollapsed).toEqual(false); }); - it("should perform the EXPAND action without mutating the previous state", () => { + it("should perform the EXPAND action without affecting the previous state", () => { const state = { navCollapsed: true }; deepFreeze(state); @@ -71,7 +78,7 @@ describe("headerReducer", () => { expect(state3.navCollapsed).toEqual(true); }); - it("should perform the TOGGLE action without mutating the previous state", () => { + it("should perform the TOGGLE action without affecting the previous state", () => { const state = { navCollapsed: true }; deepFreeze(state); diff --git a/src/app/shared/host-window.reducer.spec.ts b/src/app/shared/host-window.reducer.spec.ts index d0c2e697c0..19d4f67697 100644 --- a/src/app/shared/host-window.reducer.spec.ts +++ b/src/app/shared/host-window.reducer.spec.ts @@ -2,19 +2,27 @@ import * as deepFreeze from "deep-freeze"; import { hostWindowReducer } from "./host-window.reducer"; import { HostWindowResizeAction } from "./host-window.actions"; +class NullAction extends HostWindowResizeAction { + type = null; + + constructor() { + super(0,0); + } +} + describe('hostWindowReducer', () => { - let nullAction = new HostWindowResizeAction(0, 0); - nullAction.type = null; it("should return the current state when no valid actions have been made", () => { const state = { width: 800, height: 600 }; - const newState = hostWindowReducer(state, nullAction); + const action = new NullAction(); + const newState = hostWindowReducer(state, action); expect(newState).toEqual(state); }); it("should start with width = null and height = null", () => { - const initialState = hostWindowReducer(undefined, nullAction); + const action = new NullAction(); + const initialState = hostWindowReducer(undefined, action); expect(initialState.width).toEqual(null); expect(initialState.height).toEqual(null); @@ -29,7 +37,7 @@ describe('hostWindowReducer', () => { expect(newState.height).toEqual(768); }); - it("should perform the RESIZE action without mutating the previous state", () => { + it("should perform the RESIZE action without affecting the previous state", () => { const state = { width: 800, height: 600 }; deepFreeze(state); From e1d69e0efdfa4b3b4fd20edc479f3ee9e98e5c10 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 17 Jan 2017 18:50:07 +0100 Subject: [PATCH 4/6] header component tests --- src/app/app.component.ts | 2 +- src/app/header/header.component.spec.ts | 82 +++++++++++++++++++++++++ src/app/header/header.component.ts | 14 +---- 3 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 src/app/header/header.component.spec.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0b861bb75c..be9d1eb76c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -52,7 +52,7 @@ export class AppComponent implements OnDestroy, OnInit { @HostListener('window:resize', ['$event']) private onResize(event): void { this.store.dispatch( - new HostWindowResizeAction(event.target.innerWidth, event.target.innerHeight) + new HostWindowResizeAction(event.target.target.innerWidth, event.target.innerHeight) ); } diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts new file mode 100644 index 0000000000..f0edf5e956 --- /dev/null +++ b/src/app/header/header.component.spec.ts @@ -0,0 +1,82 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { HeaderComponent } from "./header.component"; +import { Store, StoreModule } from "@ngrx/store"; +import { HeaderState } from "./header.reducer"; +import Spy = jasmine.Spy; +import { HeaderToggleAction } from "./header.actions"; +import { TranslateModule } from "ng2-translate"; +import { NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap"; +import { Observable } from "rxjs"; + +let comp: HeaderComponent; +let fixture: ComponentFixture; +let store: Store; + +describe('HeaderComponent', () => { + + // async beforeEach + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ StoreModule.provideStore({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot() ], + declarations: [ HeaderComponent ] + }) + .compileComponents(); // compile template and css + })); + + // synchronous beforeEach + beforeEach(() => { + fixture = TestBed.createComponent(HeaderComponent); + + comp = fixture.componentInstance; + + + store = fixture.debugElement.injector.get(Store); + spyOn(store, 'dispatch'); + }); + + describe('when the toggle button is clicked', () => { + + beforeEach(() => { + let navbarToggler = fixture.debugElement.query(By.css('.navbar-toggler')); + navbarToggler.triggerEventHandler('click', null); + }); + + it("should dispatch a HeaderToggleAction", () => { + expect(store.dispatch).toHaveBeenCalledWith(new HeaderToggleAction()); + }); + + }); + + describe("when navCollapsed in the store is true", () => { + let menu: HTMLElement; + + beforeEach(() => { + menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement; + spyOn(store, 'select').and.returnValue(Observable.of({ navCollapsed: true })); + fixture.detectChanges(); + }); + + it("should close the menu", () => { + expect(menu.classList).not.toContain('in'); + }); + + }); + + describe("when navCollapsed in the store is false", () => { + let menu: HTMLElement; + + beforeEach(() => { + menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement; + spyOn(store, 'select').and.returnValue(Observable.of({ navCollapsed: false })); + fixture.detectChanges(); + }); + + it("should open the menu", () => { + expect(menu.classList).toContain('in'); + }); + + }); + +}); diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 29cac9cadd..76a9cbe05b 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -2,11 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { Store } from "@ngrx/store"; import { HeaderState } from "./header.reducer"; import { Observable } from "rxjs"; -import { - HeaderCollapseAction, - HeaderExpandAction, - HeaderToggleAction -} from "./header.actions"; +import { HeaderToggleAction } from "./header.actions"; @Component({ selector: 'ds-header', @@ -27,14 +23,6 @@ export class HeaderComponent implements OnInit { .map(({ navCollapsed }: HeaderState) => navCollapsed); } - private collapse(): void { - this.store.dispatch(new HeaderCollapseAction()); - } - - private expand(): void { - this.store.dispatch(new HeaderExpandAction()); - } - public toggle(): void { this.store.dispatch(new HeaderToggleAction()); } From 58b4b381e71a7f3d6fbd42ded597faf83d65204f Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 18 Jan 2017 11:27:39 +0100 Subject: [PATCH 5/6] added test for window resize to app.component.spec.ts --- src/app/app.component.spec.ts | 38 ++++++++++++++++++------- src/app/app.component.ts | 2 +- src/app/header/header.component.spec.ts | 1 - 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index f457d141e5..9db1e49c47 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -11,14 +11,14 @@ import { } from "@angular/core"; import { By } from '@angular/platform-browser'; import { TranslateModule } from "ng2-translate"; -import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; -import { Store } from "@ngrx/store"; +import { Store, StoreModule } from "@ngrx/store"; // Load the implementations that should be tested import { AppComponent } from './app.component'; -import { HeaderComponent } from './header/header.component'; import { CommonModule } from '@angular/common'; +import { HostWindowState } from "./shared/host-window.reducer"; +import { HostWindowResizeAction } from "./shared/host-window.actions"; let comp: AppComponent; let fixture: ComponentFixture; @@ -30,14 +30,10 @@ describe('App component', () => { // async beforeEach beforeEach(async(() => { return TestBed.configureTestingModule({ - imports: [ CommonModule, TranslateModule.forRoot(), NgbCollapseModule.forRoot()], - declarations: [ AppComponent, HeaderComponent ], // declare the test component + imports: [ CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot() ], + declarations: [ AppComponent ], // declare the test component providers: [ - AppComponent, - { - provide: Store, - useClass: class { dispatch = jasmine.createSpy('dispatch') } - } + AppComponent ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ] }) @@ -47,7 +43,7 @@ describe('App component', () => { beforeEach(() => { fixture = TestBed.createComponent(AppComponent); - comp = fixture.componentInstance; // BannerComponent test instance + comp = fixture.componentInstance; // component test instance // query for the title

by CSS element selector de = fixture.debugElement.query(By.css('p')); @@ -58,4 +54,24 @@ describe('App component', () => { // Perform test using fixture and service expect(app).toBeTruthy(); })); + + describe("when the window is resized", () => { + let width: number; + let height: number; + let store: Store; + + beforeEach(() => { + store = fixture.debugElement.injector.get(Store); + spyOn(store, 'dispatch'); + + window.dispatchEvent(new Event('resize')); + width = window.innerWidth; + height = window.innerHeight; + }); + + it("should dispatch a HostWindowResizeAction with the width and height of the window as its payload", () => { + expect(store.dispatch).toHaveBeenCalledWith(new HostWindowResizeAction(width, height)); + }); + + }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index be9d1eb76c..0b861bb75c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -52,7 +52,7 @@ export class AppComponent implements OnDestroy, OnInit { @HostListener('window:resize', ['$event']) private onResize(event): void { this.store.dispatch( - new HostWindowResizeAction(event.target.target.innerWidth, event.target.innerHeight) + new HostWindowResizeAction(event.target.innerWidth, event.target.innerHeight) ); } diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts index f0edf5e956..ab0e591bc9 100644 --- a/src/app/header/header.component.spec.ts +++ b/src/app/header/header.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; import { HeaderComponent } from "./header.component"; import { Store, StoreModule } from "@ngrx/store"; import { HeaderState } from "./header.reducer"; From 8fd1c6de5c75e95077625b7ce65f43f3b285da0c Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 18 Jan 2017 12:53:09 +0100 Subject: [PATCH 6/6] added a MockTranslateLoader to fix 404 with ng2-translate during testing --- src/app/app.component.spec.ts | 18 +++++++++++------- .../shared/testing/mock-translate-loader.ts | 8 ++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 src/app/shared/testing/mock-translate-loader.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 9db1e49c47..2664b5f27b 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -10,7 +10,7 @@ import { DebugElement } from "@angular/core"; import { By } from '@angular/platform-browser'; -import { TranslateModule } from "ng2-translate"; +import { TranslateModule, TranslateLoader } from "ng2-translate"; import { Store, StoreModule } from "@ngrx/store"; // Load the implementations that should be tested @@ -19,23 +19,27 @@ import { AppComponent } from './app.component'; import { CommonModule } from '@angular/common'; import { HostWindowState } from "./shared/host-window.reducer"; import { HostWindowResizeAction } from "./shared/host-window.actions"; +import { MockTranslateLoader } from "./shared/testing/mock-translate-loader"; -let comp: AppComponent; +let comp: AppComponent; let fixture: ComponentFixture; -let de: DebugElement; -let el: HTMLElement; +let de: DebugElement; +let el: HTMLElement; describe('App component', () => { // async beforeEach beforeEach(async(() => { return TestBed.configureTestingModule({ - imports: [ CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot() ], - declarations: [ AppComponent ], // declare the test component + imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({ + provide: TranslateLoader, + useClass: MockTranslateLoader + })], + declarations: [AppComponent], // declare the test component providers: [ AppComponent ], - schemas: [ CUSTOM_ELEMENTS_SCHEMA ] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) })); diff --git a/src/app/shared/testing/mock-translate-loader.ts b/src/app/shared/testing/mock-translate-loader.ts new file mode 100644 index 0000000000..a780766b25 --- /dev/null +++ b/src/app/shared/testing/mock-translate-loader.ts @@ -0,0 +1,8 @@ +import { TranslateLoader } from "ng2-translate"; +import { Observable } from "rxjs"; + +export class MockTranslateLoader implements TranslateLoader { + getTranslation(lang: string): Observable { + return Observable.of({}); + } +}