forked from johnpapa/angular-ngrx-data
-
Notifications
You must be signed in to change notification settings - Fork 0
/
entity-selectors.spec.ts
194 lines (156 loc) · 7.24 KB
/
entity-selectors.spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import { MemoizedSelector } from '@ngrx/store';
import { EntityCache } from '../reducers/entity-cache';
import { ENTITY_CACHE_NAME } from '../reducers/constants';
import { EntityCollection } from '../reducers/entity-collection';
import { createEmptyEntityCollection } from '../reducers/entity-collection-creator';
import { EntityMetadata, EntityMetadataMap } from '../entity-metadata/entity-metadata';
import { PropsFilterFnFactory } from '../entity-metadata/entity-filters';
import { EntitySelectors, EntitySelectorsFactory } from './entity-selectors';
describe('EntitySelectors', () => {
/** HeroMetadata identifies the extra collection state properties */
const heroMetadata: EntityMetadata<Hero> = {
entityName: 'Hero',
filterFn: nameFilter,
additionalCollectionState: {
foo: 'Foo',
bar: 3.14
}
};
const villainMetadata: EntityMetadata<Villain> = {
entityName: 'Villain',
selectId: (entity: Villain) => entity.key
};
let collectionCreator: any;
let entitySelectorsFactory: EntitySelectorsFactory;
beforeEach(() => {
collectionCreator = jasmine.createSpyObj('entityCollectionCreator', ['create']);
entitySelectorsFactory = new EntitySelectorsFactory(collectionCreator);
});
describe('#createCollectionSelector', () => {
const initialState = createHeroState({
ids: [1],
entities: { 1: { id: 1, name: 'A' } },
foo: 'foo foo',
bar: 42
});
it('creates collection selector that defaults to initial state', () => {
collectionCreator.create.and.returnValue(initialState);
const selectors = entitySelectorsFactory.createCollectionSelector<Hero, HeroCollection>('Hero');
const state = { entityCache: {} }; // ngrx store with empty cache
const collection = selectors(state);
expect(collection.entities).toEqual(initialState.entities, 'entities');
expect(collection.foo).toEqual('foo foo', 'foo');
expect(collectionCreator.create).toHaveBeenCalled();
});
it('collection selector should return cached collection when it exists', () => {
// must specify type-args when initialState isn't available for type inference
const selectors = entitySelectorsFactory.createCollectionSelector<Hero, HeroCollection>('Hero');
// ngrx store with populated Hero collection
const state = {
entityCache: {
Hero: {
ids: [42],
entities: { 42: { id: 42, name: 'The Answer' } },
filter: '',
loading: true,
foo: 'towel',
bar: 0
}
}
};
const collection = selectors(state);
expect(collection.entities[42]).toEqual({ id: 42, name: 'The Answer' }, 'entities');
expect(collection.foo).toBe('towel', 'foo');
expect(collectionCreator.create).not.toHaveBeenCalled();
});
});
describe('#createEntitySelectors', () => {
let heroCollection: HeroCollection;
let heroEntities: Hero[];
beforeEach(() => {
heroEntities = [{ id: 42, name: 'A' }, { id: 48, name: 'B' }];
heroCollection = <HeroCollection>(<any>{
ids: [42, 48],
entities: {
42: heroEntities[0],
48: heroEntities[1]
},
filter: 'B',
foo: 'Foo'
});
});
it('should have expected Hero selectors (a super-set of EntitySelectors)', () => {
const store = { entityCache: { Hero: heroCollection } };
const selectors = entitySelectorsFactory.create<Hero, HeroSelectors>(heroMetadata);
expect(selectors.selectEntities).toBeDefined('selectEntities');
expect(selectors.selectEntities(store)).toEqual(heroEntities, 'selectEntities');
expect(selectors.selectFilteredEntities(store)).toEqual(heroEntities.filter(h => h.name === 'B'), 'filtered B heroes');
expect(selectors.selectFoo).toBeDefined('selectFoo exists');
expect(selectors.selectFoo(store)).toBe('Foo', 'execute `selectFoo`');
});
it('should have all Hero when create EntitySelectorFactory directly', () => {
const store = { entityCache: { Hero: heroCollection } };
// Create EntitySelectorFactory directly rather than injecting it!
// Works ONLY if have not changed the name of the EntityCache.
// In this case, where also not supplying the EntityCollectionCreator
// selector for additional collection properties might fail,
// but doesn't in this test because the additional Foo property is in the store.
const eaFactory = new EntitySelectorsFactory();
const selectors = eaFactory.create<Hero, HeroSelectors>(heroMetadata);
expect(selectors.selectEntities).toBeDefined('selectEntities');
expect(selectors.selectEntities(store)).toEqual(heroEntities, 'selectEntities');
expect(selectors.selectFilteredEntities(store)).toEqual(heroEntities.filter(h => h.name === 'B'), 'filtered B heroes');
expect(selectors.selectFoo).toBeDefined('selectFoo exists');
expect(selectors.selectFoo(store)).toBe('Foo', 'execute `selectFoo`');
});
it('should create default selectors (no filter, no extras) when create with "Hero" instead of hero metadata', () => {
const store = { entityCache: { Hero: heroCollection } };
// const selectors = entitySelectorsFactory.create<Hero, HeroSelectors>('Hero');
// There won't be extra selectors so type selectors for Hero collection only
const selectors = entitySelectorsFactory.create<Hero>('Hero');
expect(selectors.selectEntities).toBeDefined('selectEntities');
expect(selectors.selectFoo).not.toBeDefined('selectFoo should not exist');
expect(selectors.selectFilteredEntities(store)).toEqual(heroEntities, 'filtered same as all hero entities');
});
it('should have expected Villain selectors', () => {
const collection = <EntityCollection<Villain>>(<any>{
ids: [24],
entities: { 24: { key: 'evil', name: 'A' } },
filter: 'B' // doesn't matter because no filter function
});
const store = { entityCache: { Villain: collection } };
const selectors = entitySelectorsFactory.create<Villain>(villainMetadata);
const expectedEntities: Villain[] = [{ key: 'evil', name: 'A' }];
expect(selectors.selectEntities).toBeDefined('selectAll');
expect(selectors.selectEntities(store)).toEqual(expectedEntities, 'try selectAll');
expect(selectors.selectFilteredEntities(store)).toEqual(expectedEntities, 'all villains because no filter fn');
});
});
});
/////// Test values and helpers /////////
function createHeroState(state: Partial<HeroCollection>): HeroCollection {
return { ...createEmptyEntityCollection<Hero>('Hero'), ...state } as HeroCollection;
}
function nameFilter<T>(entities: T[], pattern: string) {
return PropsFilterFnFactory<any>(['name'])(entities, pattern);
}
/// Hero
interface Hero {
id: number;
name: string;
}
/** HeroCollection is EntityCollection<Hero> with extra collection properties */
interface HeroCollection extends EntityCollection<Hero> {
foo: string;
bar: number;
}
/** HeroSelectors identifies the extra selectors for the extra collection properties */
interface HeroSelectors extends EntitySelectors<Hero> {
selectFoo: MemoizedSelector<Object, string>;
selectBar: MemoizedSelector<Object, number>;
}
/// Villain
interface Villain {
key: string;
name: string;
}