mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
added tests for hostReducer and Header Effects, and switch to typed actions
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
npm-debug.log
|
||||
|
||||
/dist/
|
||||
/coverage/
|
||||
|
||||
.idea
|
||||
*.ngfactory.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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
});
|
||||
})
|
||||
|
||||
});
|
@@ -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
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
53
src/app/header/header.effects.spec.ts
Normal file
53
src/app/header/header.effects.spec.ts
Normal file
@@ -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());
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@@ -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());
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
|
@@ -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
|
||||
});
|
||||
|
@@ -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;
|
||||
|
40
src/app/shared/host-window.reducer.spec.ts
Normal file
40
src/app/shared/host-window.reducer.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
|
||||
});
|
@@ -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);
|
||||
}
|
||||
|
||||
|
24
src/app/shared/ngrx/type.ts
Normal file
24
src/app/shared/ngrx/type.ts
Normal file
@@ -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<T>(label: T | ''): T {
|
||||
if (typeCache[<string>label]) {
|
||||
throw new Error(`Action type "${label}" is not unique"`);
|
||||
}
|
||||
|
||||
typeCache[<string>label] = true;
|
||||
|
||||
return <T>label;
|
||||
}
|
Reference in New Issue
Block a user