diff --git a/src/app/app.component.html b/src/app/app.component.html index e368a5ab6b..272b879d68 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,4 @@ -
+
{ + const testState = { theme: { name: 'test theme', cssClass: 'test-class' } }; + deepFreeze(testState); + + it('should return the current state when no valid actions have been made', () => { + const action = new NullAction(); + const newState = themeReducer(testState, action); + + expect(newState).toEqual(testState); + }); + + it('should start with an empty object', () => { + const action = new NullAction(); + const initialState = themeReducer(undefined, action); + + expect(initialState).toEqual({} as any); + }); + + it('should perform the SET action without affecting the previous state', () => { + const action = new SetThemeAction(newTheme); + // testState has already been frozen above + themeReducer(testState, action); + }); + + it('should return a new state with the new theme when calling the SET action with this new theme', () => { + const action = new SetThemeAction(newTheme); + + const newState = themeReducer(testState, action); + expect(newState.theme).toEqual(newTheme); + }); +}); diff --git a/src/app/core/theme/theme.reducer.ts b/src/app/core/theme/theme.reducer.ts index fb3e0eb7b8..26f717872d 100644 --- a/src/app/core/theme/theme.reducer.ts +++ b/src/app/core/theme/theme.reducer.ts @@ -1,6 +1,9 @@ import { Theme } from '../../../config/theme.inferface'; import { ThemeAction, ThemeActionTypes } from './theme.actions'; +/** + * Represents the state of the current theme in the store + */ export interface ThemeState { theme: Theme } @@ -8,12 +11,16 @@ export interface ThemeState { // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) const initialState = Object.create(null); +/** + * Reducer the current theme state to the new state depending on a given action + * @param state The previous state, equal to the initial state when it was not defined before + * @param action The action to perform on the current theme state + */ export function themeReducer(state = initialState, action: ThemeAction): ThemeState { switch (action.type) { case ThemeActionTypes.SET: { - const newState = Object.assign({}, state, { theme: action.payload.theme }); - console.log(newState); - return newState; + return Object.assign({}, state, { theme: action.payload.theme }); } } + return state; } diff --git a/src/app/core/theme/theme.service.spec.ts b/src/app/core/theme/theme.service.spec.ts new file mode 100644 index 0000000000..3edbc764a3 --- /dev/null +++ b/src/app/core/theme/theme.service.spec.ts @@ -0,0 +1,60 @@ +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ThemeService } from './theme.service'; +import { SetThemeAction } from './theme.actions'; +import * as ngrx from '@ngrx/store'; +import { Theme } from '../../../config/theme.inferface'; +import { of as observableOf } from 'rxjs'; +import { first } from 'rxjs/operators'; + +describe('ThemeService', () => { + let service: ThemeService; + let store: Store; + const initialTheme: Theme = { + name: 'test theme', + cssClass: 'test-class' + }; + const config = { + themes: [initialTheme] + } as any; + beforeEach(() => { + store = new Store(undefined, undefined, undefined); + spyOn(store, 'dispatch'); + service = new ThemeService(store, config); + }); + + describe('when the service is created', () => { + beforeEach(() => { + spyOn(ThemeService.prototype, 'setCurrentTheme'); + service = new ThemeService(store, config); + }); + + it('should call setCurrentTheme action on itself with the theme from the configuration', () => { + expect(service.setCurrentTheme).toHaveBeenCalledWith(initialTheme); + }); + }); + + describe('when setCurrentTheme op the service is called', () => { + it('should dispatch a SET action on the store', () => { + service.setCurrentTheme(initialTheme); + expect(store.dispatch).toHaveBeenCalledWith(new SetThemeAction(initialTheme)); + }); + }); + + describe('when getCurrentTheme op the service is called', () => { + beforeEach(() => { + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf({ theme: initialTheme }); + }; + }); + }); + + it('should select the current theme from the store', () => { + const theme = service.getCurrentTheme(); + theme.pipe(first()).subscribe((newTheme) => { + expect(newTheme).toEqual(initialTheme); + }); + }); + }); +}); diff --git a/src/app/core/theme/theme.service.ts b/src/app/core/theme/theme.service.ts index 998fe6b856..2da6a28f38 100644 --- a/src/app/core/theme/theme.service.ts +++ b/src/app/core/theme/theme.service.ts @@ -6,9 +6,11 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ThemeState } from './theme.reducer'; import { SetThemeAction } from './theme.actions'; +import { isNotEmpty } from '../../shared/empty.util'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; function themeStateSelector(): MemoizedSelector { - return createSelector(coreSelector, (state: CoreState) => state['theme']); + return createSelector(coreSelector, (state: CoreState) => state.theme); } /** @@ -16,10 +18,21 @@ function themeStateSelector(): MemoizedSelector { */ @Injectable() export class ThemeService { - constructor(private store: Store) { - + /** + * Sets the initial theme read from configuration + * @param store The current store for the core state + * @param config The global configuration + */ + constructor(private store: Store, @Inject(GLOBAL_CONFIG) public config: GlobalConfig) { + const availableThemes: Theme[] = this.config.themes; + if (isNotEmpty(availableThemes)) { + this.setCurrentTheme(availableThemes[0]); + } } + /** + * Returns the current theme from the store + */ public getCurrentTheme(): Observable { return this.store.pipe( select(themeStateSelector()), @@ -27,7 +40,11 @@ export class ThemeService { ); } + /** + * Sets the current theme in the store + * @param theme The new theme + */ public setCurrentTheme(theme: Theme): void { return this.store.dispatch(new SetThemeAction(theme)); } -} \ No newline at end of file +}