From 2a9b0672c014562eb1d5b5210d10e5db943f74f4 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 4 Apr 2019 06:56:00 -0500 Subject: [PATCH 1/4] feat(store): add API to mock selectors (#1688) Closes #1504 --- modules/store/spec/selector.spec.ts | 14 +++ modules/store/spec/store.spec.ts | 104 +++++++++++++++++- modules/store/src/selector.ts | 20 +++- modules/store/testing/src/mock_store.ts | 64 +++++++++++ .../components/login-form.component.spec.ts | 1 - .../login-page.component.spec.ts.snap | 2 +- .../containers/login-page.component.spec.ts | 19 +--- .../auth/services/auth-guard.service.spec.ts | 30 ++--- .../collection-page.component.spec.ts.snap | 2 +- .../find-book-page.component.spec.ts.snap | 2 +- .../selected-book-page.component.spec.ts.snap | 6 +- .../collection-page.component.spec.ts | 20 ++-- .../find-book-page.component.spec.ts | 23 ++-- .../selected-book-page.component.spec.ts | 23 +--- .../view-book-page.component.spec.ts | 24 ++-- .../containers/view-book-page.component.ts | 2 +- tsconfig.json | 1 + 17 files changed, 251 insertions(+), 106 deletions(-) diff --git a/modules/store/spec/selector.spec.ts b/modules/store/spec/selector.spec.ts index cd6559eb87..4f24b2f7a4 100644 --- a/modules/store/spec/selector.spec.ts +++ b/modules/store/spec/selector.spec.ts @@ -47,6 +47,20 @@ describe('Selectors', () => { expect(projectFn).toHaveBeenCalledWith(countOne, countTwo); }); + it('should allow an override of the selector return', () => { + const projectFn = jasmine.createSpy('projectionFn').and.returnValue(2); + + const selector = createSelector(incrementOne, incrementTwo, projectFn); + + expect(selector.projector()).toBe(2); + + selector.setResult(5); + + const result2 = selector({}); + + expect(result2).toBe(5); + }); + it('should be possible to test a projector fn independent from the selectors it is composed of', () => { const projectFn = jasmine.createSpy('projectionFn'); const selector = createSelector(incrementOne, incrementTwo, projectFn); diff --git a/modules/store/spec/store.spec.ts b/modules/store/spec/store.spec.ts index 9af15062ef..10b7d97326 100644 --- a/modules/store/spec/store.spec.ts +++ b/modules/store/spec/store.spec.ts @@ -26,6 +26,7 @@ import Spy = jasmine.Spy; import any = jasmine.any; import { skip, take } from 'rxjs/operators'; import { MockStore, provideMockStore } from '../testing'; +import { createSelector } from '../src/selector'; interface TestAppSchema { counter1: number; @@ -448,10 +449,9 @@ describe('ngRx Store', () => { describe('Mock Store', () => { let mockStore: MockStore; + const initialState = { counter1: 0, counter2: 1 }; beforeEach(() => { - const initialState = { counter1: 0, counter2: 1 }; - TestBed.configureTestingModule({ providers: [provideMockStore({ initialState })], }); @@ -482,6 +482,106 @@ describe('ngRx Store', () => { .subscribe(scannedAction => expect(scannedAction).toEqual(action)); mockStore.dispatch(action); }); + + it('should allow mocking of store.select with string selector', () => { + const mockValue = 5; + + mockStore.overrideSelector('counter1', mockValue); + + mockStore + .select('counter1') + .subscribe(result => expect(result).toBe(mockValue)); + }); + + it('should allow mocking of store.select with a memoized selector', () => { + const mockValue = 5; + const selector = createSelector( + () => initialState, + state => state.counter1 + ); + + mockStore.overrideSelector(selector, mockValue); + + mockStore + .select(selector) + .subscribe(result => expect(result).toBe(mockValue)); + }); + + it('should allow mocking of store.pipe(select()) with a memoized selector', () => { + const mockValue = 5; + const selector = createSelector( + () => initialState, + state => state.counter2 + ); + + mockStore.overrideSelector(selector, mockValue); + + mockStore + .pipe(select(selector)) + .subscribe(result => expect(result).toBe(mockValue)); + }); + + it('should pass through unmocked selectors', () => { + const mockValue = 5; + const selector = createSelector( + () => initialState, + state => state.counter1 + ); + const selector2 = createSelector( + () => initialState, + state => state.counter2 + ); + const selector3 = createSelector( + selector, + selector2, + (sel1, sel2) => sel1 + sel2 + ); + + mockStore.overrideSelector(selector, mockValue); + + mockStore + .pipe(select(selector2)) + .subscribe(result => expect(result).toBe(1)); + mockStore + .pipe(select(selector3)) + .subscribe(result => expect(result).toBe(6)); + }); + + it('should allow you reset mocked selectors', () => { + const mockValue = 5; + const selector = createSelector( + () => initialState, + state => state.counter1 + ); + const selector2 = createSelector( + () => initialState, + state => state.counter2 + ); + const selector3 = createSelector( + selector, + selector2, + (sel1, sel2) => sel1 + sel2 + ); + + mockStore + .pipe(select(selector3)) + .subscribe(result => expect(result).toBe(1)); + + mockStore.overrideSelector(selector, mockValue); + mockStore.overrideSelector(selector2, mockValue); + selector3.release(); + + mockStore + .pipe(select(selector3)) + .subscribe(result => expect(result).toBe(10)); + + mockStore.resetSelectors(); + selector3.release(); + + mockStore + .pipe(select(selector3)) + .subscribe(result => expect(result).toBe(1)); + }); }); describe('Meta Reducers', () => { diff --git a/modules/store/src/selector.ts b/modules/store/src/selector.ts index b369e0cef1..97bb2dbe3a 100644 --- a/modules/store/src/selector.ts +++ b/modules/store/src/selector.ts @@ -2,7 +2,11 @@ import { Selector, SelectorWithProps } from './models'; export type AnyFn = (...args: any[]) => any; -export type MemoizedProjection = { memoized: AnyFn; reset: () => void }; +export type MemoizedProjection = { + memoized: AnyFn; + reset: () => void; + setResult: (result?: any) => void; +}; export type MemoizeFn = (t: AnyFn) => MemoizedProjection; @@ -12,12 +16,14 @@ export interface MemoizedSelector extends Selector { release(): void; projector: AnyFn; + setResult: (result?: Result) => void; } export interface MemoizedSelectorWithProps extends SelectorWithProps { release(): void; projector: AnyFn; + setResult: (result?: Result) => void; } export function isEqualCheck(a: any, b: any): boolean { @@ -52,14 +58,23 @@ export function defaultMemoize( let lastArguments: null | IArguments = null; // tslint:disable-next-line:no-any anything could be the result. let lastResult: any = null; + let overrideResult: any; function reset() { lastArguments = null; lastResult = null; } + function setResult(result: any = undefined) { + overrideResult = result; + } + // tslint:disable-next-line:no-any anything could be the result. function memoized(): any { + if (overrideResult !== undefined) { + return overrideResult; + } + if (!lastArguments) { lastResult = projectionFn.apply(null, arguments); lastArguments = arguments; @@ -82,7 +97,7 @@ export function defaultMemoize( return newResult; } - return { memoized, reset }; + return { memoized, reset, setResult }; } export function createSelector( @@ -563,6 +578,7 @@ export function createSelectorFactory( return Object.assign(memoizedState.memoized, { release, projector: memoizedProjector.memoized, + setResult: memoizedState.setResult, }); }; } diff --git a/modules/store/testing/src/mock_store.ts b/modules/store/testing/src/mock_store.ts index d311ad4e8c..506d57755e 100644 --- a/modules/store/testing/src/mock_store.ts +++ b/modules/store/testing/src/mock_store.ts @@ -6,11 +6,21 @@ import { INITIAL_STATE, ReducerManager, Store, + createSelector, + MemoizedSelectorWithProps, + MemoizedSelector, } from '@ngrx/store'; import { MockState } from './mock_state'; @Injectable() export class MockStore extends Store { + static selectors = new Map< + | string + | MemoizedSelector + | MemoizedSelectorWithProps, + any + >(); + public scannedActions$: Observable; constructor( @@ -20,6 +30,7 @@ export class MockStore extends Store { @Inject(INITIAL_STATE) private initialState: T ) { super(state$, actionsObserver, reducerManager); + this.resetSelectors(); this.state$.next(this.initialState); this.scannedActions$ = actionsObserver.asObservable(); } @@ -28,6 +39,59 @@ export class MockStore extends Store { this.state$.next(nextState); } + overrideSelector( + selector: string, + value: Result + ): MemoizedSelector; + overrideSelector( + selector: MemoizedSelector, + value: Result + ): MemoizedSelector; + overrideSelector( + selector: MemoizedSelectorWithProps, + value: Result + ): MemoizedSelectorWithProps; + overrideSelector( + selector: + | string + | MemoizedSelector + | MemoizedSelectorWithProps, + value: any + ) { + MockStore.selectors.set(selector, value); + + if (typeof selector === 'string') { + const stringSelector = createSelector(() => {}, () => value); + + return stringSelector; + } + + selector.setResult(value); + + return selector; + } + + resetSelectors() { + MockStore.selectors.forEach((_, selector) => { + if (typeof selector !== 'string') { + selector.release(); + selector.setResult(); + } + }); + + MockStore.selectors.clear(); + } + + select(selector: any) { + if (MockStore.selectors.has(selector)) { + return new BehaviorSubject( + MockStore.selectors.get(selector) + ).asObservable(); + } + + return super.select(selector); + } + addReducer() { /* noop */ } diff --git a/projects/example-app/src/app/auth/components/login-form.component.spec.ts b/projects/example-app/src/app/auth/components/login-form.component.spec.ts index f0e852f382..454cfaebd0 100644 --- a/projects/example-app/src/app/auth/components/login-form.component.spec.ts +++ b/projects/example-app/src/app/auth/components/login-form.component.spec.ts @@ -1,6 +1,5 @@ import { TestBed, ComponentFixture } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { StoreModule, Store, combineReducers } from '@ngrx/store'; import { LoginFormComponent } from '@example-app/auth/components/login-form.component'; import { ReactiveFormsModule } from '@angular/forms'; diff --git a/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap b/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap index 21c53f9d7b..043cef9ce5 100644 --- a/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap +++ b/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap @@ -4,7 +4,7 @@ exports[`Login Page should compile 1`] = ` { let fixture: ComponentFixture; - let store: Store; + let store: MockStore; let instance: LoginPageComponent; beforeEach(() => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, - StoreModule.forRoot( - { - auth: combineReducers(fromAuth.reducers), - }, - { - runtimeChecks: { - strictImmutability: true, - }, - } - ), MatInputModule, MatCardModule, ReactiveFormsModule, ], declarations: [LoginPageComponent, LoginFormComponent], + providers: [provideMockStore()], }); fixture = TestBed.createComponent(LoginPageComponent); instance = fixture.componentInstance; store = TestBed.get(Store); + store.overrideSelector(fromAuth.getLoginPagePending, false); - spyOn(store, 'dispatch').and.callThrough(); + spyOn(store, 'dispatch'); }); /** diff --git a/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts b/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts index 4dd60b26cb..b45ecf054a 100644 --- a/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts +++ b/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts @@ -1,30 +1,24 @@ import { TestBed } from '@angular/core/testing'; -import { Store } from '@ngrx/store'; +import { Store, MemoizedSelector } from '@ngrx/store'; import { cold } from 'jasmine-marbles'; import { AuthGuard } from '@example-app/auth/services/auth-guard.service'; -import * as fromRoot from '@example-app/reducers'; import * as fromAuth from '@example-app/auth/reducers'; -import * as fromLoginPage from '@example-app/auth/reducers/login-page.reducer'; import { provideMockStore, MockStore } from '@ngrx/store/testing'; describe('Auth Guard', () => { let guard: AuthGuard; let store: MockStore; - const initialState = { - auth: { - status: { - user: null, - }, - }, - } as fromAuth.State; + let loggedIn: MemoizedSelector; beforeEach(() => { TestBed.configureTestingModule({ - providers: [AuthGuard, provideMockStore({ initialState })], + providers: [AuthGuard, provideMockStore()], }); store = TestBed.get(Store); guard = TestBed.get(AuthGuard); + + loggedIn = store.overrideSelector(fromAuth.getLoggedIn, false); }); it('should return false if the user state is not logged in', () => { @@ -34,20 +28,10 @@ describe('Auth Guard', () => { }); it('should return true if the user state is logged in', () => { - store.setState({ - ...initialState, - auth: { - loginPage: {} as fromLoginPage.State, - status: { - user: { - name: 'John', - }, - }, - }, - }); - const expected = cold('(a|)', { a: true }); + loggedIn.setResult(true); + expect(guard.canActivate()).toBeObservable(expected); }); }); diff --git a/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap b/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap index 9fb38ee108..321d019614 100644 --- a/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap +++ b/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap @@ -3,7 +3,7 @@ exports[`Collection Page should compile 1`] = ` - + diff --git a/projects/example-app/src/app/books/containers/collection-page.component.spec.ts b/projects/example-app/src/app/books/containers/collection-page.component.spec.ts index d4acfbb590..3057aad4cd 100644 --- a/projects/example-app/src/app/books/containers/collection-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/collection-page.component.spec.ts @@ -1,5 +1,5 @@ import { CollectionPageComponent } from '@example-app/books/containers/collection-page.component'; -import { combineReducers, Store, StoreModule } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; @@ -11,26 +11,17 @@ import * as fromBooks from '@example-app/books/reducers'; import { EllipsisPipe } from '@example-app/shared/pipes/ellipsis.pipe'; import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; import { BookAuthorsComponent } from '@example-app/books/components/book-authors.component'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; describe('Collection Page', () => { let fixture: ComponentFixture; - let store: Store; + let store: MockStore; let instance: CollectionPageComponent; beforeEach(() => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, - StoreModule.forRoot( - { - books: combineReducers(fromBooks.reducers), - }, - { - runtimeChecks: { - strictImmutability: true, - }, - } - ), MatCardModule, MatInputModule, RouterTestingModule, @@ -43,16 +34,19 @@ describe('Collection Page', () => { AddCommasPipe, EllipsisPipe, ], + providers: [provideMockStore()], }); fixture = TestBed.createComponent(CollectionPageComponent); instance = fixture.componentInstance; store = TestBed.get(Store); - spyOn(store, 'dispatch').and.callThrough(); + spyOn(store, 'dispatch'); }); it('should compile', () => { + store.overrideSelector(fromBooks.getBookCollection, []); + fixture.detectChanges(); expect(fixture).toMatchSnapshot(); diff --git a/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts index 6986f1cbf2..cf54889e54 100644 --- a/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts @@ -5,7 +5,7 @@ import { MatInputModule, MatProgressSpinnerModule, } from '@angular/material'; -import { combineReducers, Store, StoreModule } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { BookSearchComponent } from '@example-app/books/components/book-search.component'; @@ -18,26 +18,17 @@ import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; import { FindBookPageComponent } from '@example-app/books/containers/find-book-page.component'; import { FindBookPageActions } from '@example-app/books/actions'; import * as fromBooks from '@example-app/books/reducers'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; describe('Find Book Page', () => { let fixture: ComponentFixture; - let store: Store; + let store: MockStore; let instance: FindBookPageComponent; beforeEach(() => { TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, - StoreModule.forRoot( - { - books: combineReducers(fromBooks.reducers), - }, - { - runtimeChecks: { - strictImmutability: true, - }, - } - ), RouterTestingModule, MatInputModule, MatCardModule, @@ -53,13 +44,19 @@ describe('Find Book Page', () => { AddCommasPipe, EllipsisPipe, ], + providers: [provideMockStore()], }); fixture = TestBed.createComponent(FindBookPageComponent); instance = fixture.componentInstance; store = TestBed.get(Store); - spyOn(store, 'dispatch').and.callThrough(); + spyOn(store, 'dispatch'); + + store.overrideSelector(fromBooks.getSearchQuery, ''); + store.overrideSelector(fromBooks.getSearchResults, []); + store.overrideSelector(fromBooks.getSearchLoading, false); + store.overrideSelector(fromBooks.getSearchError, ''); }); it('should compile', () => { diff --git a/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts index 751dc1eb0b..24dd13a227 100644 --- a/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SelectedBookPageComponent } from '@example-app/books/containers/selected-book-page.component'; -import { combineReducers, Store, StoreModule } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatCardModule } from '@angular/material'; @@ -10,41 +10,30 @@ import { BookDetailComponent } from '@example-app/books/components/book-detail.c import { Book, generateMockBook } from '@example-app/books/models/book'; import { BookAuthorsComponent } from '@example-app/books/components/book-authors.component'; import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; describe('Selected Book Page', () => { let fixture: ComponentFixture; - let store: Store; + let store: MockStore; let instance: SelectedBookPageComponent; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - NoopAnimationsModule, - StoreModule.forRoot( - { - books: combineReducers(fromBooks.reducers), - }, - { - runtimeChecks: { - strictImmutability: true, - }, - } - ), - MatCardModule, - ], + imports: [NoopAnimationsModule, MatCardModule], declarations: [ SelectedBookPageComponent, BookDetailComponent, BookAuthorsComponent, AddCommasPipe, ], + providers: [provideMockStore()], }); fixture = TestBed.createComponent(SelectedBookPageComponent); instance = fixture.componentInstance; store = TestBed.get(Store); - spyOn(store, 'dispatch').and.callThrough(); + spyOn(store, 'dispatch'); }); it('should compile', () => { diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts index c66c5dba32..4fea62f70e 100644 --- a/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts @@ -11,12 +11,13 @@ import { SelectedBookPageComponent } from '@example-app/books/containers/selecte import { BookDetailComponent } from '@example-app/books/components/book-detail.component'; import { BookAuthorsComponent } from '@example-app/books/components/book-authors.component'; import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; describe('View Book Page', () => { - const params = new BehaviorSubject({}); let fixture: ComponentFixture; - let store: Store; + let store: MockStore; let instance: ViewBookPageComponent; + let route: ActivatedRoute; beforeEach(() => { TestBed.configureTestingModule({ @@ -24,16 +25,9 @@ describe('View Book Page', () => { providers: [ { provide: ActivatedRoute, - useValue: { params }, - }, - { - provide: Store, - useValue: { - select: jest.fn(), - next: jest.fn(), - pipe: jest.fn(), - }, + useValue: { params: new BehaviorSubject({}) }, }, + provideMockStore(), ], declarations: [ ViewBookPageComponent, @@ -47,6 +41,9 @@ describe('View Book Page', () => { fixture = TestBed.createComponent(ViewBookPageComponent); instance = fixture.componentInstance; store = TestBed.get(Store); + route = TestBed.get(ActivatedRoute); + + jest.spyOn(store, 'dispatch'); }); it('should compile', () => { @@ -57,10 +54,9 @@ describe('View Book Page', () => { it('should dispatch a book.Select action on init', () => { const action = ViewBookPageActions.selectBook({ id: '2' }); - params.next({ id: '2' }); - fixture.detectChanges(); + (route.params as BehaviorSubject).next({ id: '2' }); - expect(store.next).toHaveBeenLastCalledWith(action); + expect(store.dispatch).toHaveBeenLastCalledWith(action); }); }); diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.ts b/projects/example-app/src/app/books/containers/view-book-page.component.ts index b1e13c353b..2c4f82fcb7 100644 --- a/projects/example-app/src/app/books/containers/view-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/view-book-page.component.ts @@ -30,7 +30,7 @@ export class ViewBookPageComponent implements OnDestroy { constructor(store: Store, route: ActivatedRoute) { this.actionsSubscription = route.params .pipe(map(params => ViewBookPageActions.selectBook({ id: params.id }))) - .subscribe(store); + .subscribe(action => store.dispatch(action)); } ngOnDestroy() { diff --git a/tsconfig.json b/tsconfig.json index d2a395d8d8..6b8643a40c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ "@ngrx/store": ["./modules/store"], "@ngrx/store/testing": ["./modules/store/testing"], "@ngrx/store/schematics-core": ["./modules/store/schematics-core"], + "@ngrx/store/testing": ["./modules/store/testing"], "@ngrx/store-devtools": ["./modules/store-devtools"], "@ngrx/store-devtools/schematics-core": [ "./modules/store-devtools/schematics-core" From f185d1a3d3239c454f76283f3b336e41d11e0146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20Andr=C3=A9s=20L=C3=B3pez=20Molina?= Date: Thu, 4 Apr 2019 15:43:50 -0500 Subject: [PATCH 2/4] docs: add Spartacus to resources page (#1696) Closes #1661 --- projects/ngrx.io/content/marketing/resources.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/projects/ngrx.io/content/marketing/resources.json b/projects/ngrx.io/content/marketing/resources.json index c246127fa0..93769adf90 100644 --- a/projects/ngrx.io/content/marketing/resources.json +++ b/projects/ngrx.io/content/marketing/resources.json @@ -107,6 +107,13 @@ "Angular CRUD application starter with NgRx state management and Firebase backend", "url": "https://github.com/mdbootstrap/Angular-Bootstrap-Boilerplate", "rev": true + }, + "sap-spartacus": { + "title": "Spartacus", + "desc": + "Spartacus is a lean, Angular-based JavaScript storefront for SAP Commerce Cloud that communicates exclusively through the Commerce REST API.", + "url": "https://github.com/SAP/cloud-commerce-spartacus-storefront", + "rev": true } } }, From 45a5781f6e88181a6f00e68977869579f2d06976 Mon Sep 17 00:00:00 2001 From: John Crowson Date: Thu, 4 Apr 2019 16:44:34 -0400 Subject: [PATCH 3/4] docs: order past events in descending order (#1695) --- .../events/event-list.component.spec.ts | 3 +- .../events/event-list.component.ts | 4 +- .../events/event-list.module.ts | 3 +- .../events/event-order-by.pipe.spec.ts | 178 ++++++++++++++++++ .../events/event-order-by.pipe.ts | 25 +++ 5 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 projects/ngrx.io/src/app/custom-elements/events/event-order-by.pipe.spec.ts create mode 100644 projects/ngrx.io/src/app/custom-elements/events/event-order-by.pipe.ts diff --git a/projects/ngrx.io/src/app/custom-elements/events/event-list.component.spec.ts b/projects/ngrx.io/src/app/custom-elements/events/event-list.component.spec.ts index 3c6a39c13a..f524ce3d25 100644 --- a/projects/ngrx.io/src/app/custom-elements/events/event-list.component.spec.ts +++ b/projects/ngrx.io/src/app/custom-elements/events/event-list.component.spec.ts @@ -4,6 +4,7 @@ import { of } from 'rxjs'; import { EventService } from './event.service'; import { Event } from './event.model'; import { EventDateRangePipe } from './event-date-range.pipe'; +import { EventOrderByPipe } from './event-order-by.pipe'; const mockUpcomingEvents: Event[] = [ { @@ -35,7 +36,7 @@ describe('Event List Component', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [ EventListComponent, EventDateRangePipe ], + declarations: [ EventListComponent, EventDateRangePipe, EventOrderByPipe ], providers: [{ provide: EventService, useClass: TestEventService }] }); diff --git a/projects/ngrx.io/src/app/custom-elements/events/event-list.component.ts b/projects/ngrx.io/src/app/custom-elements/events/event-list.component.ts index 08e5fa117c..09e0bdc5ce 100644 --- a/projects/ngrx.io/src/app/custom-elements/events/event-list.component.ts +++ b/projects/ngrx.io/src/app/custom-elements/events/event-list.component.ts @@ -16,7 +16,7 @@ import { Observable } from 'rxjs'; - + {{upcomingEvent.name}} {{upcomingEvent.location}} {{upcomingEvent | eventDateRange}} @@ -33,7 +33,7 @@ import { Observable } from 'rxjs'; - + {{pastEvent.name}} {{pastEvent.location}} {{pastEvent | eventDateRange}} diff --git a/projects/ngrx.io/src/app/custom-elements/events/event-list.module.ts b/projects/ngrx.io/src/app/custom-elements/events/event-list.module.ts index 0a5e8cb060..f3098955de 100644 --- a/projects/ngrx.io/src/app/custom-elements/events/event-list.module.ts +++ b/projects/ngrx.io/src/app/custom-elements/events/event-list.module.ts @@ -4,10 +4,11 @@ import { WithCustomElementComponent } from '../element-registry'; import { EventListComponent } from './event-list.component'; import { EventService } from './event.service'; import { EventDateRangePipe } from './event-date-range.pipe'; +import { EventOrderByPipe } from './event-order-by.pipe'; @NgModule({ imports: [ CommonModule ], - declarations: [ EventListComponent, EventDateRangePipe ], + declarations: [ EventListComponent, EventDateRangePipe, EventOrderByPipe ], entryComponents: [ EventListComponent ], providers: [ EventService ] }) diff --git a/projects/ngrx.io/src/app/custom-elements/events/event-order-by.pipe.spec.ts b/projects/ngrx.io/src/app/custom-elements/events/event-order-by.pipe.spec.ts new file mode 100644 index 0000000000..47f22555ec --- /dev/null +++ b/projects/ngrx.io/src/app/custom-elements/events/event-order-by.pipe.spec.ts @@ -0,0 +1,178 @@ +import { Event } from './event.model'; +import { EventOrderByPipe } from './event-order-by.pipe'; + +describe('Pipe: Event Order By', () => { + let pipe: EventOrderByPipe; + + beforeEach(() => { + pipe = new EventOrderByPipe(); + }); + + it('should return an empty array if the passed events array is null', () => { + expect(pipe.transform(null, 'ascending')).toEqual([]); + }); + + describe('ascending', () => { + it('should order an event with an earlier startDate before an event with a later startDate, ' + + 'regardless of endDate', () => { + const earlierEvent: Event = { + name: '', + url: '', + location: '', + startDate: new Date('2019-01-01'), + endDate: new Date('2019-01-04') + }; + + const laterEvent: Event = { + name: '', + url: '', + location: '', + startDate: new Date('2019-01-02'), + endDate: new Date('2019-01-03') + }; + + expect(pipe.transform([laterEvent, earlierEvent], 'ascending')).toEqual([earlierEvent, laterEvent]); + }); + + it('should order an event with only an endDate before an event with a startDate ' + + 'if the first events endDate is before the second events startDate', () => { + const earlierEvent: Event = { + name: '', + url: '', + location: '', + endDate: new Date('2019-01-02') + }; + + const laterEvent: Event = { + name: '', + url: '', + location: '', + startDate: new Date('2019-01-03'), + endDate: new Date('2019-01-04') + }; + + expect(pipe.transform([laterEvent, earlierEvent], 'ascending')).toEqual([earlierEvent, laterEvent]); + }); + + it('should order an event with a startDate before an event with only an endDate ' + + 'if the first events startDate is before the second events endDate', () => { + const earlierEvent: Event = { + name: '', + url: '', + location: '', + startDate: new Date('2019-01-01'), + endDate: new Date('2019-01-03') + }; + + const laterEvent: Event = { + name: '', + url: '', + location: '', + endDate: new Date('2019-01-02') + }; + + expect(pipe.transform([laterEvent, earlierEvent], 'ascending')).toEqual([earlierEvent, laterEvent]); + }); + + it('should order an event with only an endDate before an event with only an endDate ' + + 'if the first events endDate is before the second events endDate', () => { + const earlierEvent: Event = { + name: '', + url: '', + location: '', + endDate: new Date('2019-01-01') + }; + + const laterEvent: Event = { + name: '', + url: '', + location: '', + endDate: new Date('2019-01-02') + }; + + expect(pipe.transform([laterEvent, earlierEvent], 'ascending')).toEqual([earlierEvent, laterEvent]); + }); + }); + + describe('descending', () => { + it('should order an event with an earlier startDate after an event with a later startDate, ' + + 'regardless of endDate', () => { + const earlierEvent: Event = { + name: '', + url: '', + location: '', + startDate: new Date('2019-01-01'), + endDate: new Date('2019-01-04') + }; + + const laterEvent: Event = { + name: '', + url: '', + location: '', + startDate: new Date('2019-01-02'), + endDate: new Date('2019-01-03') + }; + + expect(pipe.transform([earlierEvent, laterEvent], 'descending')).toEqual([laterEvent, earlierEvent]); + }); + + it('should order an event with only an endDate after an event with a startDate ' + + 'if the first events endDate is before the second events startDate', () => { + const earlierEvent: Event = { + name: '', + url: '', + location: '', + endDate: new Date('2019-01-02') + }; + + const laterEvent: Event = { + name: '', + url: '', + location: '', + startDate: new Date('2019-01-03'), + endDate: new Date('2019-01-04') + }; + + expect(pipe.transform([earlierEvent, laterEvent], 'descending')).toEqual([laterEvent, earlierEvent]); + }); + + it('should order an event with a startDate after an event with only an endDate ' + + 'if the first events startDate is before the second events endDate', () => { + const earlierEvent: Event = { + name: '', + url: '', + location: '', + startDate: new Date('2019-01-01'), + endDate: new Date('2019-01-03') + }; + + const laterEvent: Event = { + name: '', + url: '', + location: '', + endDate: new Date('2019-01-02') + }; + + expect(pipe.transform([earlierEvent, laterEvent], 'descending')).toEqual([laterEvent, earlierEvent]); + }); + + it('should order an event with only an endDate after an event with only an endDate ' + + 'if the first events endDate is before the second events endDate', () => { + const earlierEvent: Event = { + name: '', + url: '', + location: '', + endDate: new Date('2019-01-01') + }; + + const laterEvent: Event = { + name: '', + url: '', + location: '', + endDate: new Date('2019-01-02') + }; + + expect(pipe.transform([earlierEvent, laterEvent], 'descending')).toEqual([laterEvent, earlierEvent]); + }); + }); +}); diff --git a/projects/ngrx.io/src/app/custom-elements/events/event-order-by.pipe.ts b/projects/ngrx.io/src/app/custom-elements/events/event-order-by.pipe.ts new file mode 100644 index 0000000000..290ddb458a --- /dev/null +++ b/projects/ngrx.io/src/app/custom-elements/events/event-order-by.pipe.ts @@ -0,0 +1,25 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Event } from './event.model'; + +type EventOrderBy = 'ascending' | 'descending'; + +/** + * Transforms the events to sorted ascending or descending order by date. + * If an event has a startDate, order based on it. If not, use it's endDate. + */ +@Pipe({name: 'eventOrderBy'}) +export class EventOrderByPipe implements PipeTransform { + transform(events: Event[] | null, orderBy: EventOrderBy): Event[] { + if (events === null) { + return []; + } + switch (orderBy) { + case 'ascending': { + return events.sort((eventOne, eventTwo) => +(eventOne.startDate || eventOne.endDate) - +(eventTwo.startDate || eventTwo.endDate)); + } + case 'descending': { + return events.sort((eventOne, eventTwo) => +(eventTwo.startDate || eventTwo.endDate) - +(eventOne.startDate || eventOne.endDate)); + } + } + } +} From c9c9a0e4f91e01b4cbffb1e00ad3161093f590b0 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Thu, 4 Apr 2019 16:47:07 -0400 Subject: [PATCH 4/4] feat(example): update ofType in effects per #1676 (#1691) Closes #1676 --- .../example-app/src/app/auth/effects/auth.effects.ts | 10 +++++----- .../example-app/src/app/books/effects/book.effects.ts | 6 +++--- .../src/app/books/effects/collection.effects.ts | 10 ++++------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/projects/example-app/src/app/auth/effects/auth.effects.ts b/projects/example-app/src/app/auth/effects/auth.effects.ts index 919a43e767..3d0146f086 100644 --- a/projects/example-app/src/app/auth/effects/auth.effects.ts +++ b/projects/example-app/src/app/auth/effects/auth.effects.ts @@ -17,7 +17,7 @@ import { LogoutConfirmationDialogComponent } from '@example-app/auth/components/ export class AuthEffects { login$ = createEffect(() => this.actions$.pipe( - ofType(LoginPageActions.login.type), + ofType(LoginPageActions.login), map(action => action.credentials), exhaustMap((auth: Credentials) => this.authService.login(auth).pipe( @@ -31,7 +31,7 @@ export class AuthEffects { loginSuccess$ = createEffect( () => this.actions$.pipe( - ofType(AuthApiActions.loginSuccess.type), + ofType(AuthApiActions.loginSuccess), tap(() => this.router.navigate(['/'])) ), { dispatch: false } @@ -40,7 +40,7 @@ export class AuthEffects { loginRedirect$ = createEffect( () => this.actions$.pipe( - ofType(AuthApiActions.loginRedirect.type, AuthActions.logout.type), + ofType(AuthApiActions.loginRedirect, AuthActions.logout), tap(authed => { this.router.navigate(['/login']); }) @@ -50,7 +50,7 @@ export class AuthEffects { logoutConfirmation$ = createEffect(() => this.actions$.pipe( - ofType(AuthActions.logoutConfirmation.type), + ofType(AuthActions.logoutConfirmation), exhaustMap(() => { const dialogRef = this.dialog.open< LogoutConfirmationDialogComponent, @@ -70,7 +70,7 @@ export class AuthEffects { ); constructor( - private actions$: Actions, + private actions$: Actions, private authService: AuthService, private router: Router, private dialog: MatDialog diff --git a/projects/example-app/src/app/books/effects/book.effects.ts b/projects/example-app/src/app/books/effects/book.effects.ts index ef20a5b22f..53c6d460aa 100644 --- a/projects/example-app/src/app/books/effects/book.effects.ts +++ b/projects/example-app/src/app/books/effects/book.effects.ts @@ -33,7 +33,7 @@ export class BookEffects { search$ = createEffect( () => ({ debounce = 300, scheduler = asyncScheduler } = {}) => this.actions$.pipe( - ofType(FindBookPageActions.searchBooks.type), + ofType(FindBookPageActions.searchBooks), debounceTime(debounce, scheduler), switchMap(({ query }) => { if (query === '') { @@ -41,7 +41,7 @@ export class BookEffects { } const nextSearch$ = this.actions$.pipe( - ofType(FindBookPageActions.searchBooks.type), + ofType(FindBookPageActions.searchBooks), skip(1) ); @@ -57,7 +57,7 @@ export class BookEffects { ); constructor( - private actions$: Actions, + private actions$: Actions, private googleBooks: GoogleBooksService ) {} } diff --git a/projects/example-app/src/app/books/effects/collection.effects.ts b/projects/example-app/src/app/books/effects/collection.effects.ts index d0646d7a7d..04a71c02c5 100644 --- a/projects/example-app/src/app/books/effects/collection.effects.ts +++ b/projects/example-app/src/app/books/effects/collection.effects.ts @@ -26,7 +26,7 @@ export class CollectionEffects { loadCollection$ = createEffect(() => this.actions$.pipe( - ofType(CollectionPageActions.loadCollection.type), + ofType(CollectionPageActions.loadCollection), switchMap(() => this.storageService.getCollection().pipe( map((books: Book[]) => @@ -42,7 +42,7 @@ export class CollectionEffects { addBookToCollection$ = createEffect(() => this.actions$.pipe( - ofType(SelectedBookPageActions.addBook.type), + ofType(SelectedBookPageActions.addBook), mergeMap(({ book }) => this.storageService.addToCollection([book]).pipe( map(() => CollectionApiActions.addBookSuccess({ book })), @@ -54,7 +54,7 @@ export class CollectionEffects { removeBookFromCollection$ = createEffect(() => this.actions$.pipe( - ofType(SelectedBookPageActions.removeBook.type), + ofType(SelectedBookPageActions.removeBook), mergeMap(({ book }) => this.storageService.removeFromCollection([book.id]).pipe( map(() => CollectionApiActions.removeBookSuccess({ book })), @@ -65,9 +65,7 @@ export class CollectionEffects { ); constructor( - private actions$: Actions< - SelectedBookPageActions.SelectedBookPageActionsUnion - >, + private actions$: Actions, private storageService: BookStorageService ) {} }