Skip to content

Commit ee2c114

Browse files
feat(store): testing - clean up mock store and remove static property (#2361)
1 parent 5e84b37 commit ee2c114

File tree

2 files changed

+116
-56
lines changed

2 files changed

+116
-56
lines changed

modules/store/testing/spec/mock_store.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,17 @@ describe('Refreshing state', () => {
344344
expect(getTodoItems('p').length).toBe(1);
345345
expect(getTodoItems('p')[0].nativeElement.textContent.trim()).toBe('bbb');
346346
});
347+
348+
it('should work with overrideSelector twice', () => {
349+
const newTodos = [{ name: 'bbb', done: true }];
350+
mockStore.overrideSelector(todos, newTodos);
351+
mockStore.refreshState();
352+
353+
fixture.detectChanges();
354+
355+
expect(getTodoItems('li').length).toBe(1);
356+
expect(getTodoItems('li')[0].nativeElement.textContent.trim()).toBe('bbb');
357+
});
347358
});
348359

349360
describe('Cleans up after each test', () => {
@@ -389,3 +400,62 @@ describe('Cleans up after each test', () => {
389400
});
390401
});
391402
});
403+
404+
describe('Resets selectors after each test', () => {
405+
const selectorUnderTest = createSelector(
406+
(state: any) => state,
407+
state => state.value
408+
);
409+
let shouldSetMockStore = true;
410+
411+
function setupModules(isMock: boolean) {
412+
if (isMock) {
413+
TestBed.configureTestingModule({
414+
providers: [
415+
provideMockStore({
416+
initialState: {
417+
value: 100,
418+
},
419+
selectors: [{ selector: selectorUnderTest, value: 200 }],
420+
}),
421+
],
422+
});
423+
} else {
424+
TestBed.configureTestingModule({
425+
imports: [
426+
StoreModule.forRoot({} as any, {
427+
initialState: {
428+
value: 300,
429+
},
430+
}),
431+
],
432+
});
433+
}
434+
}
435+
436+
/**
437+
* Tests run in random order, so that's why we have two attempts (one runs
438+
* before another - in any order) and whichever runs the test first would
439+
* setup MockStore and override selector. The next one would use the regular
440+
* Store and verifies that the selector is cleared/reset.
441+
*/
442+
it('should reset selector - attempt one', (done: DoneFn) => {
443+
setupModules(shouldSetMockStore);
444+
const store: Store<{}> = TestBed.get<Store<{}>>(Store);
445+
store.select(selectorUnderTest).subscribe(v => {
446+
expect(v).toBe(shouldSetMockStore ? 200 : 300);
447+
shouldSetMockStore = false;
448+
done();
449+
});
450+
});
451+
452+
it('should reset selector - attempt two', (done: DoneFn) => {
453+
setupModules(shouldSetMockStore);
454+
const store: Store<{}> = TestBed.get<Store<{}>>(Store);
455+
store.select(selectorUnderTest).subscribe(v => {
456+
expect(v).toBe(shouldSetMockStore ? 200 : 300);
457+
shouldSetMockStore = false;
458+
done();
459+
});
460+
});
461+
});

modules/store/testing/src/mock_store.ts

Lines changed: 46 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,46 +18,44 @@ import { MOCK_SELECTORS } from './tokens';
1818
if (typeof afterEach === 'function') {
1919
afterEach(() => {
2020
try {
21-
const store = TestBed.get(Store) as MockStore<any>;
22-
if (store && 'resetSelectors' in store) {
23-
store.resetSelectors();
21+
const mockStore: MockStore | undefined = TestBed.inject(MockStore);
22+
if (mockStore) {
23+
mockStore.resetSelectors();
2424
}
2525
} catch {}
2626
});
2727
}
2828

29+
type OnlyMemoized<T, Result> = T extends string | MemoizedSelector<any, any>
30+
? MemoizedSelector<any, Result>
31+
: T extends MemoizedSelectorWithProps<any, any, any>
32+
? MemoizedSelectorWithProps<any, any, Result>
33+
: never;
34+
35+
type Memoized<Result> =
36+
| MemoizedSelector<any, Result>
37+
| MemoizedSelectorWithProps<any, any, Result>;
38+
2939
@Injectable()
3040
export class MockStore<T = object> extends Store<T> {
31-
static selectors = new Map<
32-
| string
33-
| MemoizedSelector<any, any>
34-
| MemoizedSelectorWithProps<any, any, any>,
35-
any
36-
>();
37-
38-
scannedActions$: Observable<Action>;
41+
private readonly selectors = new Map<Memoized<any> | string, any>();
42+
43+
readonly scannedActions$: Observable<Action>;
3944
private lastState?: T;
4045

4146
constructor(
4247
private state$: MockState<T>,
4348
actionsObserver: ActionsSubject,
4449
reducerManager: ReducerManager,
4550
@Inject(INITIAL_STATE) private initialState: T,
46-
@Inject(MOCK_SELECTORS) mockSelectors?: MockSelector[]
51+
@Inject(MOCK_SELECTORS) mockSelectors: MockSelector[] = []
4752
) {
4853
super(state$, actionsObserver, reducerManager);
4954
this.resetSelectors();
5055
this.setState(this.initialState);
5156
this.scannedActions$ = actionsObserver.asObservable();
52-
if (mockSelectors) {
53-
mockSelectors.forEach(mockSelector => {
54-
const selector = mockSelector.selector;
55-
if (typeof selector === 'string') {
56-
this.overrideSelector(selector, mockSelector.value);
57-
} else {
58-
this.overrideSelector(selector, mockSelector.value);
59-
}
60-
});
57+
for (const mockSelector of mockSelectors) {
58+
this.overrideSelector(mockSelector.selector, mockSelector.value);
6159
}
6260
}
6361

@@ -66,53 +64,45 @@ export class MockStore<T = object> extends Store<T> {
6664
this.lastState = nextState;
6765
}
6866

69-
overrideSelector<T, Result>(
70-
selector: string,
71-
value: Result
72-
): MemoizedSelector<string, Result>;
73-
overrideSelector<T, Result>(
74-
selector: MemoizedSelector<T, Result>,
75-
value: Result
76-
): MemoizedSelector<T, Result>;
77-
overrideSelector<T, Result>(
78-
selector: MemoizedSelectorWithProps<T, any, Result>,
79-
value: Result
80-
): MemoizedSelectorWithProps<T, any, Result>;
81-
overrideSelector<T, Result>(
82-
selector:
83-
| string
84-
| MemoizedSelector<any, any>
85-
| MemoizedSelectorWithProps<any, any, any>,
86-
value: any
87-
) {
88-
MockStore.selectors.set(selector, value);
89-
90-
if (typeof selector === 'string') {
91-
const stringSelector = createSelector(() => {}, () => value);
92-
93-
return stringSelector;
94-
}
95-
96-
selector.setResult(value);
97-
98-
return selector;
67+
overrideSelector<
68+
Selector extends Memoized<Result>,
69+
Value extends Result,
70+
Result = Selector extends MemoizedSelector<any, infer T>
71+
? T
72+
: Selector extends MemoizedSelectorWithProps<any, any, infer U>
73+
? U
74+
: Value
75+
>(
76+
selector: Selector | string,
77+
value: Value
78+
): OnlyMemoized<typeof selector, Result> {
79+
this.selectors.set(selector, value);
80+
81+
const resultSelector: Memoized<Result> =
82+
typeof selector === 'string'
83+
? createSelector(() => {}, (): Result => value)
84+
: selector;
85+
86+
resultSelector.setResult(value);
87+
88+
return resultSelector as OnlyMemoized<typeof selector, Result>;
9989
}
10090

10191
resetSelectors() {
102-
MockStore.selectors.forEach((_, selector) => {
92+
for (const selector of this.selectors.keys()) {
10393
if (typeof selector !== 'string') {
10494
selector.release();
10595
selector.clearResult();
10696
}
107-
});
97+
}
10898

109-
MockStore.selectors.clear();
99+
this.selectors.clear();
110100
}
111101

112102
select(selector: any, prop?: any) {
113-
if (typeof selector === 'string' && MockStore.selectors.has(selector)) {
103+
if (typeof selector === 'string' && this.selectors.has(selector)) {
114104
return new BehaviorSubject<any>(
115-
MockStore.selectors.get(selector)
105+
this.selectors.get(selector)
116106
).asObservable();
117107
}
118108

0 commit comments

Comments
 (0)