diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index bd2d832c67..c29b1fb2fb 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -40,6 +40,7 @@ import { CSSVariableServiceStub } from './shared/testing/css-variable-service-st
import { MenuServiceStub } from './shared/testing/menu-service-stub';
import { HostWindowService } from './shared/host-window.service';
import { HostWindowServiceStub } from './shared/testing/host-window-service-stub';
+import { ThemeService } from './core/theme/theme.service';
import { ActivatedRoute, Router } from '@angular/router';
import { RouteService } from './shared/services/route.service';
import { MockActivatedRoute } from './shared/mocks/mock-active-router';
@@ -78,6 +79,7 @@ describe('App component', () => {
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
+ { provide: ThemeService, useValue: {getCurrentTheme: () => {/* No implementation */}} },
AppComponent,
RouteService
],
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 65f59f49a8..7abc1aab97 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -34,7 +34,6 @@ import { combineLatest as combineLatestObservable, of } from 'rxjs';
import { HostWindowService } from './shared/host-window.service';
import { ThemeService } from './core/theme/theme.service';
import { Theme } from '../config/theme.inferface';
-import { isNotEmpty } from './shared/empty.util';
@Component({
selector: 'ds-app',
@@ -92,10 +91,6 @@ export class AppComponent implements OnInit, AfterViewInit {
}
ngOnInit() {
- const availableThemes: Theme[] = this.config.themes;
- if (isNotEmpty(availableThemes)) {
- this.themeService.setCurrentTheme(availableThemes[0]);
- }
this.theme = this.themeService.getCurrentTheme();
const env: string = this.config.production ? 'Production' : 'Development';
diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts
index 94669cac31..6f4eed018b 100644
--- a/src/app/core/core.effects.ts
+++ b/src/app/core/core.effects.ts
@@ -5,7 +5,6 @@ import { AuthEffects } from './auth/auth.effects';
import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects';
import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
-import { ThemeEffects } from './theme/theme.effects';
export const coreEffects = [
RequestEffects,
@@ -15,5 +14,4 @@ export const coreEffects = [
JsonPatchOperationsEffects,
ServerSyncBufferEffects,
ObjectUpdatesEffects,
- ThemeEffects
];
diff --git a/src/app/core/theme/theme.effects.ts b/src/app/core/theme/theme.effects.ts
deleted file mode 100644
index f03b356b78..0000000000
--- a/src/app/core/theme/theme.effects.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Inject, Injectable } from '@angular/core';
-import { Action, Store } from '@ngrx/store';
-import { Actions, Effect } from '@ngrx/effects';
-import { CoreState } from '../core.reducers';
-import { SetThemeAction } from './theme.actions';
-import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
-import { Theme } from '../../../config/theme.inferface';
-import { isNotEmpty } from '../../shared/empty.util';
-import { asyncScheduler, defer, Observable, of as observableOf } from 'rxjs';
-
-@Injectable()
-export class ThemeEffects {
-
- // @Effect() setInitialTheme: Observable
= defer(() => {
- // console.log('set theme');
- // const availableThemes: Theme[] = this.config.themes;
- // if (isNotEmpty(availableThemes)) {
- // return observableOf(new SetThemeAction(availableThemes[0]), asyncScheduler);
- // }
- // });
- //
- // constructor(private actions: Actions, private store: Store, @Inject(GLOBAL_CONFIG) public config: GlobalConfig) {
- // console.log("theme effects");
- // }
-}
diff --git a/src/app/core/theme/theme.reducer.spec.ts b/src/app/core/theme/theme.reducer.spec.ts
new file mode 100644
index 0000000000..662f9bb852
--- /dev/null
+++ b/src/app/core/theme/theme.reducer.spec.ts
@@ -0,0 +1,46 @@
+import * as deepFreeze from 'deep-freeze';
+import { SetThemeAction } from './theme.actions';
+import { themeReducer } from './theme.reducer';
+import { Theme } from '../../../config/theme.inferface';
+
+class NullAction extends SetThemeAction {
+ type = null;
+ payload = null;
+
+ constructor() {
+ super(null);
+ }
+}
+
+const newTheme: Theme = { name: 'New theme', cssClass: 'new-class' };
+describe('themeReducer', () => {
+ 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
+}
diff --git a/webpack/webpack.test.js b/webpack/webpack.test.js
index 8c6760e377..233fb2be57 100644
--- a/webpack/webpack.test.js
+++ b/webpack/webpack.test.js
@@ -165,7 +165,8 @@ module.exports = function (options) {
options: {
sourceMap: true
}
- }
+ },
+ 'webpack-import-glob-loader'
],
exclude: [root('src/index.html')]
},