Commits

Kristof De Langhe authored 2d638a738e9
83628: Dynamic theme fixes
No tags

src/app/shared/theme-support/theme.effects.spec.ts

Modified
1 1 import { ThemeEffects } from './theme.effects';
2 -import { of as observableOf } from 'rxjs';
3 2 import { TestBed } from '@angular/core/testing';
4 3 import { provideMockActions } from '@ngrx/effects/testing';
5 -import { LinkService } from '../../core/cache/builders/link.service';
6 4 import { cold, hot } from 'jasmine-marbles';
7 5 import { ROOT_EFFECTS_INIT } from '@ngrx/effects';
8 6 import { SetThemeAction } from './theme.actions';
9 -import { Theme } from '../../../config/theme.model';
10 7 import { provideMockStore } from '@ngrx/store/testing';
11 -import { ROUTER_NAVIGATED } from '@ngrx/router-store';
12 -import { ResolverActionTypes } from '../../core/resolving/resolver.actions';
13 -import { Community } from '../../core/shared/community.model';
14 -import { COMMUNITY } from '../../core/shared/community.resource-type';
15 -import { NoOpAction } from '../ngrx/no-op.action';
16 -import { ITEM } from '../../core/shared/item.resource-type';
17 -import { DSpaceObject } from '../../core/shared/dspace-object.model';
18 -import { Item } from '../../core/shared/item.model';
19 -import { Collection } from '../../core/shared/collection.model';
20 -import { COLLECTION } from '../../core/shared/collection.resource-type';
21 -import {
22 - createNoContentRemoteDataObject$,
23 - createSuccessfulRemoteDataObject$
24 -} from '../remote-data.utils';
25 8 import { BASE_THEME_NAME } from './theme.constants';
26 9
27 -/**
28 - * LinkService able to mock recursively resolving DSO parent links
29 - * Every time resolveLinkWithoutAttaching is called, it returns the next object in the array of ancestorDSOs until
30 - * none are left, after which it returns a no-content remote-date
31 - */
32 -class MockLinkService {
33 - index = -1;
34 -
35 - constructor(private ancestorDSOs: DSpaceObject[]) {
36 - }
37 -
38 - resolveLinkWithoutAttaching() {
39 - if (this.index >= this.ancestorDSOs.length - 1) {
40 - return createNoContentRemoteDataObject$();
41 - } else {
42 - this.index++;
43 - return createSuccessfulRemoteDataObject$(this.ancestorDSOs[this.index]);
44 - }
45 - }
46 -}
47 -
48 10 describe('ThemeEffects', () => {
49 11 let themeEffects: ThemeEffects;
50 - let linkService: LinkService;
51 12 let initialState;
52 13
53 - let ancestorDSOs: DSpaceObject[];
54 -
55 14 function init() {
56 - ancestorDSOs = [
57 - Object.assign(new Collection(), {
58 - type: COLLECTION.value,
59 - uuid: 'collection-uuid',
60 - _links: { owningCommunity: { href: 'owning-community-link' } }
61 - }),
62 - Object.assign(new Community(), {
63 - type: COMMUNITY.value,
64 - uuid: 'sub-community-uuid',
65 - _links: { parentCommunity: { href: 'parent-community-link' } }
66 - }),
67 - Object.assign(new Community(), {
68 - type: COMMUNITY.value,
69 - uuid: 'top-community-uuid',
70 - }),
71 - ];
72 - linkService = new MockLinkService(ancestorDSOs) as any;
73 15 initialState = {
74 16 theme: {
75 17 currentTheme: 'custom',
76 18 },
77 19 };
78 20 }
79 21
80 22 function setupEffectsWithActions(mockActions) {
81 23 init();
82 24 TestBed.configureTestingModule({
83 25 providers: [
84 26 ThemeEffects,
85 - { provide: LinkService, useValue: linkService },
86 27 provideMockStore({ initialState }),
87 28 provideMockActions(() => mockActions)
88 29 ]
89 30 });
90 31
91 32 themeEffects = TestBed.inject(ThemeEffects);
92 33 }
93 34
94 35 describe('initTheme$', () => {
95 36 beforeEach(() => {
103 44 });
104 45
105 46 it('should set the default theme', () => {
106 47 const expected = cold('--b-', {
107 48 b: new SetThemeAction(BASE_THEME_NAME)
108 49 });
109 50
110 51 expect(themeEffects.initTheme$).toBeObservable(expected);
111 52 });
112 53 });
113 -
114 - describe('updateThemeOnRouteChange$', () => {
115 - const url = '/test/route';
116 - const dso = Object.assign(new Community(), {
117 - type: COMMUNITY.value,
118 - uuid: '0958c910-2037-42a9-81c7-dca80e3892b4',
119 - });
120 -
121 - function spyOnPrivateMethods() {
122 - spyOn((themeEffects as any), 'getAncestorDSOs').and.returnValue(() => observableOf([dso]));
123 - spyOn((themeEffects as any), 'matchThemeToDSOs').and.returnValue(new Theme({ name: 'custom' }));
124 - spyOn((themeEffects as any), 'getActionForMatch').and.returnValue(new SetThemeAction('custom'));
125 - }
126 -
127 - describe('when a resolved action is present', () => {
128 - beforeEach(() => {
129 - setupEffectsWithActions(
130 - hot('--ab-', {
131 - a: {
132 - type: ROUTER_NAVIGATED,
133 - payload: { routerState: { url } },
134 - },
135 - b: {
136 - type: ResolverActionTypes.RESOLVED,
137 - payload: { url, dso },
138 - }
139 - })
140 - );
141 - spyOnPrivateMethods();
142 - });
143 -
144 - it('should set the theme it receives from the DSO', () => {
145 - const expected = cold('--b-', {
146 - b: new SetThemeAction('custom')
147 - });
148 -
149 - expect(themeEffects.updateThemeOnRouteChange$).toBeObservable(expected);
150 - });
151 - });
152 -
153 - describe('when no resolved action is present', () => {
154 - beforeEach(() => {
155 - setupEffectsWithActions(
156 - hot('--a-', {
157 - a: {
158 - type: ROUTER_NAVIGATED,
159 - payload: { routerState: { url } },
160 - },
161 - })
162 - );
163 - spyOnPrivateMethods();
164 - });
165 -
166 - it('should set the theme it receives from the route url', () => {
167 - const expected = cold('--b-', {
168 - b: new SetThemeAction('custom')
169 - });
170 -
171 - expect(themeEffects.updateThemeOnRouteChange$).toBeObservable(expected);
172 - });
173 - });
174 -
175 - describe('when no themes are present', () => {
176 - beforeEach(() => {
177 - setupEffectsWithActions(
178 - hot('--a-', {
179 - a: {
180 - type: ROUTER_NAVIGATED,
181 - payload: { routerState: { url } },
182 - },
183 - })
184 - );
185 - (themeEffects as any).themes = [];
186 - });
187 -
188 - it('should return an empty action', () => {
189 - const expected = cold('--b-', {
190 - b: new NoOpAction()
191 - });
192 -
193 - expect(themeEffects.updateThemeOnRouteChange$).toBeObservable(expected);
194 - });
195 - });
196 - });
197 -
198 - describe('private functions', () => {
199 - beforeEach(() => {
200 - setupEffectsWithActions(hot('-', {}));
201 - });
202 -
203 - describe('getActionForMatch', () => {
204 - it('should return a SET action if the new theme differs from the current theme', () => {
205 - const theme = new Theme({ name: 'new-theme' });
206 - expect((themeEffects as any).getActionForMatch(theme, 'old-theme')).toEqual(new SetThemeAction('new-theme'));
207 - });
208 -
209 - it('should return an empty action if the new theme equals the current theme', () => {
210 - const theme = new Theme({ name: 'old-theme' });
211 - expect((themeEffects as any).getActionForMatch(theme, 'old-theme')).toEqual(new NoOpAction());
212 - });
213 - });
214 -
215 - describe('matchThemeToDSOs', () => {
216 - let themes: Theme[];
217 - let nonMatchingTheme: Theme;
218 - let itemMatchingTheme: Theme;
219 - let communityMatchingTheme: Theme;
220 - let dsos: DSpaceObject[];
221 -
222 - beforeEach(() => {
223 - nonMatchingTheme = Object.assign(new Theme({ name: 'non-matching-theme' }), {
224 - matches: () => false
225 - });
226 - itemMatchingTheme = Object.assign(new Theme({ name: 'item-matching-theme' }), {
227 - matches: (url, dso) => (dso as any).type === ITEM.value
228 - });
229 - communityMatchingTheme = Object.assign(new Theme({ name: 'community-matching-theme' }), {
230 - matches: (url, dso) => (dso as any).type === COMMUNITY.value
231 - });
232 - dsos = [
233 - Object.assign(new Item(), {
234 - type: ITEM.value,
235 - uuid: 'item-uuid',
236 - }),
237 - Object.assign(new Collection(), {
238 - type: COLLECTION.value,
239 - uuid: 'collection-uuid',
240 - }),
241 - Object.assign(new Community(), {
242 - type: COMMUNITY.value,
243 - uuid: 'community-uuid',
244 - }),
245 - ];
246 - });
247 -
248 - describe('when no themes match any of the DSOs', () => {
249 - beforeEach(() => {
250 - themes = [ nonMatchingTheme ];
251 - themeEffects.themes = themes;
252 - });
253 -
254 - it('should return undefined', () => {
255 - expect((themeEffects as any).matchThemeToDSOs(dsos, '')).toBeUndefined();
256 - });
257 - });
258 -
259 - describe('when one of the themes match a DSOs', () => {
260 - beforeEach(() => {
261 - themes = [ nonMatchingTheme, itemMatchingTheme ];
262 - themeEffects.themes = themes;
263 - });
264 -
265 - it('should return the matching theme', () => {
266 - expect((themeEffects as any).matchThemeToDSOs(dsos, '')).toEqual(itemMatchingTheme);
267 - });
268 - });
269 -
270 - describe('when multiple themes match some of the DSOs', () => {
271 - it('should return the first matching theme', () => {
272 - themes = [ nonMatchingTheme, itemMatchingTheme, communityMatchingTheme ];
273 - themeEffects.themes = themes;
274 - expect((themeEffects as any).matchThemeToDSOs(dsos, '')).toEqual(itemMatchingTheme);
275 -
276 - themes = [ nonMatchingTheme, communityMatchingTheme, itemMatchingTheme ];
277 - themeEffects.themes = themes;
278 - expect((themeEffects as any).matchThemeToDSOs(dsos, '')).toEqual(communityMatchingTheme);
279 - });
280 - });
281 - });
282 -
283 - describe('getAncestorDSOs', () => {
284 - it('should return an array of the provided DSO and its ancestors', (done) => {
285 - const dso = Object.assign(new Item(), {
286 - type: ITEM.value,
287 - uuid: 'item-uuid',
288 - _links: { owningCollection: { href: 'owning-collection-link' } },
289 - });
290 -
291 - observableOf(dso).pipe(
292 - (themeEffects as any).getAncestorDSOs()
293 - ).subscribe((result) => {
294 - expect(result).toEqual([dso, ...ancestorDSOs]);
295 - done();
296 - });
297 - });
298 -
299 - it('should return an array of just the provided DSO if it doesn\'t have any parents', (done) => {
300 - const dso = {
301 - type: ITEM.value,
302 - uuid: 'item-uuid',
303 - };
304 -
305 - observableOf(dso).pipe(
306 - (themeEffects as any).getAncestorDSOs()
307 - ).subscribe((result) => {
308 - expect(result).toEqual([dso]);
309 - done();
310 - });
311 - });
312 - });
313 - });
314 54 });

Everything looks good. We'll let you know here if there's anything you should know about.

Add shortcut