Skip to content

Commit

Permalink
feat(store): testing - clean up mock store and remove static property (
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-okrushko authored Feb 17, 2020
1 parent 5e84b37 commit ee2c114
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 56 deletions.
70 changes: 70 additions & 0 deletions modules/store/testing/spec/mock_store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,17 @@ describe('Refreshing state', () => {
expect(getTodoItems('p').length).toBe(1);
expect(getTodoItems('p')[0].nativeElement.textContent.trim()).toBe('bbb');
});

it('should work with overrideSelector twice', () => {
const newTodos = [{ name: 'bbb', done: true }];
mockStore.overrideSelector(todos, newTodos);
mockStore.refreshState();

fixture.detectChanges();

expect(getTodoItems('li').length).toBe(1);
expect(getTodoItems('li')[0].nativeElement.textContent.trim()).toBe('bbb');
});
});

describe('Cleans up after each test', () => {
Expand Down Expand Up @@ -389,3 +400,62 @@ describe('Cleans up after each test', () => {
});
});
});

describe('Resets selectors after each test', () => {
const selectorUnderTest = createSelector(
(state: any) => state,
state => state.value
);
let shouldSetMockStore = true;

function setupModules(isMock: boolean) {
if (isMock) {
TestBed.configureTestingModule({
providers: [
provideMockStore({
initialState: {
value: 100,
},
selectors: [{ selector: selectorUnderTest, value: 200 }],
}),
],
});
} else {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({} as any, {
initialState: {
value: 300,
},
}),
],
});
}
}

/**
* Tests run in random order, so that's why we have two attempts (one runs
* before another - in any order) and whichever runs the test first would
* setup MockStore and override selector. The next one would use the regular
* Store and verifies that the selector is cleared/reset.
*/
it('should reset selector - attempt one', (done: DoneFn) => {
setupModules(shouldSetMockStore);
const store: Store<{}> = TestBed.get<Store<{}>>(Store);
store.select(selectorUnderTest).subscribe(v => {
expect(v).toBe(shouldSetMockStore ? 200 : 300);
shouldSetMockStore = false;
done();
});
});

it('should reset selector - attempt two', (done: DoneFn) => {
setupModules(shouldSetMockStore);
const store: Store<{}> = TestBed.get<Store<{}>>(Store);
store.select(selectorUnderTest).subscribe(v => {
expect(v).toBe(shouldSetMockStore ? 200 : 300);
shouldSetMockStore = false;
done();
});
});
});
102 changes: 46 additions & 56 deletions modules/store/testing/src/mock_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,46 +18,44 @@ import { MOCK_SELECTORS } from './tokens';
if (typeof afterEach === 'function') {
afterEach(() => {
try {
const store = TestBed.get(Store) as MockStore<any>;
if (store && 'resetSelectors' in store) {
store.resetSelectors();
const mockStore: MockStore | undefined = TestBed.inject(MockStore);
if (mockStore) {
mockStore.resetSelectors();
}
} catch {}
});
}

type OnlyMemoized<T, Result> = T extends string | MemoizedSelector<any, any>
? MemoizedSelector<any, Result>
: T extends MemoizedSelectorWithProps<any, any, any>
? MemoizedSelectorWithProps<any, any, Result>
: never;

type Memoized<Result> =
| MemoizedSelector<any, Result>
| MemoizedSelectorWithProps<any, any, Result>;

@Injectable()
export class MockStore<T = object> extends Store<T> {
static selectors = new Map<
| string
| MemoizedSelector<any, any>
| MemoizedSelectorWithProps<any, any, any>,
any
>();

scannedActions$: Observable<Action>;
private readonly selectors = new Map<Memoized<any> | string, any>();

readonly scannedActions$: Observable<Action>;
private lastState?: T;

constructor(
private state$: MockState<T>,
actionsObserver: ActionsSubject,
reducerManager: ReducerManager,
@Inject(INITIAL_STATE) private initialState: T,
@Inject(MOCK_SELECTORS) mockSelectors?: MockSelector[]
@Inject(MOCK_SELECTORS) mockSelectors: MockSelector[] = []
) {
super(state$, actionsObserver, reducerManager);
this.resetSelectors();
this.setState(this.initialState);
this.scannedActions$ = actionsObserver.asObservable();
if (mockSelectors) {
mockSelectors.forEach(mockSelector => {
const selector = mockSelector.selector;
if (typeof selector === 'string') {
this.overrideSelector(selector, mockSelector.value);
} else {
this.overrideSelector(selector, mockSelector.value);
}
});
for (const mockSelector of mockSelectors) {
this.overrideSelector(mockSelector.selector, mockSelector.value);
}
}

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

overrideSelector<T, Result>(
selector: string,
value: Result
): MemoizedSelector<string, Result>;
overrideSelector<T, Result>(
selector: MemoizedSelector<T, Result>,
value: Result
): MemoizedSelector<T, Result>;
overrideSelector<T, Result>(
selector: MemoizedSelectorWithProps<T, any, Result>,
value: Result
): MemoizedSelectorWithProps<T, any, Result>;
overrideSelector<T, Result>(
selector:
| string
| MemoizedSelector<any, any>
| MemoizedSelectorWithProps<any, any, any>,
value: any
) {
MockStore.selectors.set(selector, value);

if (typeof selector === 'string') {
const stringSelector = createSelector(() => {}, () => value);

return stringSelector;
}

selector.setResult(value);

return selector;
overrideSelector<
Selector extends Memoized<Result>,
Value extends Result,
Result = Selector extends MemoizedSelector<any, infer T>
? T
: Selector extends MemoizedSelectorWithProps<any, any, infer U>
? U
: Value
>(
selector: Selector | string,
value: Value
): OnlyMemoized<typeof selector, Result> {
this.selectors.set(selector, value);

const resultSelector: Memoized<Result> =
typeof selector === 'string'
? createSelector(() => {}, (): Result => value)
: selector;

resultSelector.setResult(value);

return resultSelector as OnlyMemoized<typeof selector, Result>;
}

resetSelectors() {
MockStore.selectors.forEach((_, selector) => {
for (const selector of this.selectors.keys()) {
if (typeof selector !== 'string') {
selector.release();
selector.clearResult();
}
});
}

MockStore.selectors.clear();
this.selectors.clear();
}

select(selector: any, prop?: any) {
if (typeof selector === 'string' && MockStore.selectors.has(selector)) {
if (typeof selector === 'string' && this.selectors.has(selector)) {
return new BehaviorSubject<any>(
MockStore.selectors.get(selector)
this.selectors.get(selector)
).asObservable();
}

Expand Down

0 comments on commit ee2c114

Please sign in to comment.