diff --git a/goofy-client/README.md b/goofy-client/README.md index 39ee03606f61fbb52358e0d30f29fcbcbd81498c..ec41cbe04ce6dfc57a9fbf3bbcb8c5e678c2594b 100644 --- a/goofy-client/README.md +++ b/goofy-client/README.md @@ -3,7 +3,9 @@ ## Client starten Um den Client zum laufen zu bekommen, muss zunächst ein `npm install` ausgeführt werden. --> nach dem Ausführen sollte sich ein `node_modules` Ordner im Verzeichnis befinden. <br> + +-> nach dem Ausführen sollte sich ein `node_modules` Ordner im Verzeichnis befinden. + Im Anschluß wird der Client über `npm start` gestartet. --- @@ -83,15 +85,15 @@ Mit `nx` bzw. `nx --list` krieg man eine Liste alle verfügbaren, schon von nx * </br> -## Anbei ein Ausschnitt der verfügbaren Befehle/Scripte und einer kurzen Erläuterung. +## Anbei ein Ausschnitt der verfügbaren Befehle/Scripte und einer kurzen Erläuterung | Command | Description | Examples | | :------ | :------ | :----- | | `start` | Startet den Client mit dem Port **4300** und der **proxy.conf.json** | `npm start / npm run start` | `build` | Baut das Projekt(und cached den build) | `npm run build` -| `test` | Führt alle Test's aus(***app*** + ***libraries***) | `npm run test / npm test` +| `test` | Führt alle Test's aus(**app** + **ibraries**) | `npm run test / npm test` | `test:cov` | Führt alle Test's aus und zeigt am Ende eine Übersicht der Testabdeckung | `npm run test:cov` -| `lint` | Führt das ***eslint*** für die, von den lokalen Änderungen **direkt** betroffenen, libraries aus | `npm run lint` +| `lint` | Führt das **eslint** für die, von den lokalen Änderungen **direkt** betroffenen, libraries aus | `npm run lint` | `dep-graph` | Öffnet ein Fenster zur graphischen Veranschaulichung des Zusammenspielst von app, e2e und der einzelnen libraries | `npm run dep-graph` | `cypress:open` | Öffnet ein Fenster mit cpress-runner für die Integrationtest's welche auch gleich da ausgeführt werden können | `npm run cypress:open` | `test:lib` | Führt alle Test's einer library aus(mit watch mode) | `npm run test:lib vorgang` @@ -117,4 +119,28 @@ Man bekommt am Ende eine Zusammenfassung von den Warnings und Errors. Selektiert die von den lokalen Änderung betroffenen Libraries vor und stellt diese in Rot dar. </br> (sonst identisch zu `dep-graph`) -`affected:apps`, `affected:e2e`, `affected:build` beziehen sich jeweils auf ganze Projekte/Apps. \ No newline at end of file +`affected:apps`, `affected:e2e`, `affected:build` beziehen sich jeweils auf ganze Projekte/Apps. + +## **Ngrx** + +Command zum Generieren einer state. +Beispiel für den fachlichen Vorgang: + +```code +nx g @nrwl/angular:ngrx vorgang --module=libs/vorgang-shared/src/lib/vorgang.module.ts +``` + +Es wird eine Menge Testcode generiert, es ist dem entsprechend abzuwägen, ob man sich die generieren lässt oder die Struktur selber anlegt und sich das rausschmeißen des generierten Codes spart. + +Die generierten Daten kommen in ein `+state` Verzeichnis. +die Schnittstelle zu den Componenten der `service`. + +## **Marbles** + +Für Mehr Info: <https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/testing/marble-testing.md#marble-syntax> + +| Marble Syntax | Description | +| :------ | :------ | +| `'-'` | frame: 1 "frame" of virtual time passing (see above description of frames). +| `'\|'` | complete: The successful completion of an observable. This is the observable producer signaling `complete()`. +| `'#'` | error: An error terminating the observable. This is the observable producer signaling `error()`. diff --git a/goofy-client/apps/goofy-e2e/src/integration/einheitlicher-ansprechpartner/vorgang-detail/vorgang-forward.e2e-spec.ts b/goofy-client/apps/goofy-e2e/src/integration/einheitlicher-ansprechpartner/vorgang-detail/vorgang-forward.e2e-spec.ts index fda7111aa7218d76245dc2877105f887be24b773..c9cf9b93733f86b60c2be6875b0abcfa36459384 100644 --- a/goofy-client/apps/goofy-e2e/src/integration/einheitlicher-ansprechpartner/vorgang-detail/vorgang-forward.e2e-spec.ts +++ b/goofy-client/apps/goofy-e2e/src/integration/einheitlicher-ansprechpartner/vorgang-detail/vorgang-forward.e2e-spec.ts @@ -9,7 +9,7 @@ import { FORWARDING_TEST_EMAIL } from '../../../support/data.util'; import { loginAsEmil } from '../../../support/user-util'; import { buildVorgang, createVorgang, initVorgaenge, objectIds } from '../../../support/vorgang-util'; -describe('Vorgang forwarding', () => { +describe('Vorgang forward', () => { const mainPage: MainPage = new MainPage(); const vorgangList: VorgangListE2EComponent = mainPage.getVorgangList(); diff --git a/goofy-client/apps/goofy-e2e/src/integration/main-tests/postfach-mail/postfach-mail.e2e-spec.ts b/goofy-client/apps/goofy-e2e/src/integration/main-tests/postfach-mail/postfach-mail.e2e-spec.ts index 4dd1ab1f47bea635704b6688e61ca5db7baf3b47..c769dd283b81e1122fbd9313e303916d4df86a8a 100644 --- a/goofy-client/apps/goofy-e2e/src/integration/main-tests/postfach-mail/postfach-mail.e2e-spec.ts +++ b/goofy-client/apps/goofy-e2e/src/integration/main-tests/postfach-mail/postfach-mail.e2e-spec.ts @@ -12,7 +12,7 @@ import { PostfachMailItemE2E, PostfachNachrichtSnackbarMessageE2E, VorgangAttach import { MainPage, waitForSpinnerToDisappear } from '../../../page-objects/main.po'; import { PostfachMailPage } from '../../../page-objects/postfach-mail.component.po'; import { VorgangPage } from '../../../page-objects/vorgang.po'; -import { readFileFromDownloads } from '../../../support/cypress-helper'; +import { dropCollections, readFileFromDownloads } from '../../../support/cypress-helper'; import { containClass, contains, exist, notBeVisible, notContainClass, notExist, visible } from '../../../support/cypress.util'; import { TEST_FILE_WITHOUT_CONTENT, TEST_FILE_WITH_CONTENT, TEST_FILE_WITH_CONTENT_4_MB } from '../../../support/data.util'; import { uploadEmptyFile, uploadFile } from '../../../support/file-upload'; @@ -65,7 +65,7 @@ describe('PostfachMail', () => { }) after(() => { - // dropCollections(); + dropCollections(); }) describe('mail icon', () => { @@ -88,6 +88,7 @@ describe('PostfachMail', () => { describe('navigate to vorgang detail', () => { it('should open vorgang detail', () => { + waitForSpinnerToDisappear(); vorgangList.getListItem(vorgang.name).getRoot().click(); waitForSpinnerToDisappear(); @@ -348,6 +349,7 @@ describe('PostfachMail', () => { describe('navigate to vorgang detail', () => { it('should open vorgang detail', () => { + waitForSpinnerToDisappear(); vorgangList.getListItem(vorgangWithoutPostfach.name).getRoot().click(); waitForSpinnerToDisappear(); diff --git a/goofy-client/apps/goofy-e2e/src/integration/main-tests/vorgang-list/vorgang-list.search.e2e-spec.ts b/goofy-client/apps/goofy-e2e/src/integration/main-tests/vorgang-list/vorgang-list.search.e2e-spec.ts index 1fa2c0c8f192151232620a5b18288730884f2038..755e812881cba262fc23d036523a12da280a840e 100644 --- a/goofy-client/apps/goofy-e2e/src/integration/main-tests/vorgang-list/vorgang-list.search.e2e-spec.ts +++ b/goofy-client/apps/goofy-e2e/src/integration/main-tests/vorgang-list/vorgang-list.search.e2e-spec.ts @@ -108,6 +108,7 @@ describe('VorgangList Suche', () => { waitForSpinnerToDisappear(); mainPage.getVorgangSearch().getClearButton().click(); + waitForSpinnerToDisappear(); exist(vorgangList.getRoot()); exist(vorgangStayInList.getRoot()); diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.effects.spec.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.effects.spec.ts index 2cffc8ede52e3a4115181bc78fda54fe18136776..3d76f416c4944b79263d28e8b41423177238eec6 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.effects.spec.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.effects.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import { routerNavigationAction } from '@ngrx/router-store'; +import { routerNavigatedAction } from '@ngrx/router-store'; import { Action } from '@ngrx/store'; import { provideMockStore } from '@ngrx/store/testing'; import { NxModule } from '@nrwl/angular'; import { hot } from 'jest-marbles'; import { Observable } from 'rxjs'; import * as NavigationUtil from '../navigation.util'; -import { createCurrentRouteData } from './../../../test/navigation-test-factory'; +import { createRouteData } from './../../../test/navigation-test-factory'; import * as NavigationActions from './navigation.actions'; import { NavigationEffects } from './navigation.effects'; @@ -30,15 +30,15 @@ describe('NavigationEffects', () => { describe('navigate$', () => { - const action = routerNavigationAction; + const action = routerNavigatedAction; it('should dispatch updateCurrentRouteData action with data', () => { - jest.spyOn(NavigationUtil, 'buildRouteData').mockReturnValue(createCurrentRouteData()); + jest.spyOn(NavigationUtil, 'buildRouteData').mockReturnValue(createRouteData()); actions = hot('-a-|', { a: action }); effects.navigateEnd$.subscribe(); - const expected = hot('-a-|', { a: NavigationActions.updateCurrentRouteData({ routeData: createCurrentRouteData() }) }); + const expected = hot('-a-|', { a: NavigationActions.updateCurrentRouteData({ routeData: createRouteData() }) }); expect(effects.navigateEnd$).toBeObservable(expected); }) diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.effects.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.effects.ts index 7d5e2d69a6a8aace159ddc15faf4cb018127035c..87817cc7e6fa490faf65999c9b54f54cea347af5 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.effects.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.effects.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; -import { routerNavigationAction } from '@ngrx/router-store'; +import { routerNavigatedAction } from '@ngrx/router-store'; import { of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { buildRouteData } from '../navigation.util'; @@ -13,7 +13,7 @@ export class NavigationEffects { navigateEnd$ = createEffect(() => this.actions$.pipe( - ofType(routerNavigationAction), + ofType(routerNavigatedAction), switchMap((action) => { return of(NavigationActions.updateCurrentRouteData({ routeData: buildRouteData(action) })) }) diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.facade.spec.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.facade.spec.ts index d67e899de4179d5a21196edbacfb9d360e281300..8654f1dbfa6ec7e4f8c8fb3ce7c200cdbf6a24c1 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.facade.spec.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.facade.spec.ts @@ -1,61 +1,38 @@ -import { NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { EffectsModule } from '@ngrx/effects'; -import { Store, StoreModule } from '@ngrx/store'; -import { NxModule } from '@nrwl/angular'; -import { readFirst } from '@nrwl/angular/testing'; -import { of } from 'rxjs'; -import { createCurrentRouteData } from './../../../test/navigation-test-factory'; -import { NavigationEffects } from './navigation.effects'; +import { Mock, mock, useFromMock } from '@goofy-client/test-utils'; +import { Store } from '@ngrx/store'; +import { Subject } from 'rxjs'; +import { createRouteData } from './../../../test/navigation-test-factory'; import { NavigationFacade } from './navigation.facade'; import { RouteData } from './navigation.models'; -import { NavigationPartialState, NAVIGATION_FEATURE_KEY, reducer } from './navigation.reducer'; describe('NavigationFacade', () => { let facade: NavigationFacade; - let store: Store<NavigationPartialState>; + let store: Mock<Store>; + + let selectionSubject: Subject<any>; beforeEach(() => { - @NgModule({ - imports: [ - StoreModule.forFeature(NAVIGATION_FEATURE_KEY, reducer), - EffectsModule.forFeature([NavigationEffects]) - ], - providers: [NavigationFacade] - }) - class CustomFeatureModule { } - - @NgModule({ - imports: [ - NxModule.forRoot(), - StoreModule.forRoot({}), - EffectsModule.forRoot([]), - CustomFeatureModule, - ], - }) - class RootModule { } - TestBed.configureTestingModule({ imports: [RootModule] }); - - store = TestBed.inject(Store); - facade = TestBed.inject(NavigationFacade); - }); - - describe('get current route data', () => { - - it('should return null on initial state', async () => { - let routeData = await readFirst(facade.getCurrentRouteData()); - - expect(routeData).toBeNull(); - }) - - it('should return data on data in state', async () => { - const response: RouteData = createCurrentRouteData(); - (<any>store.select) = jest.fn(); - (<any>store.select).mockReturnValue(of(response)); - - let routeData = await readFirst(facade.getCurrentRouteData()); - - expect(routeData).toBe(response); - }) + store = mock(Store); + + selectionSubject = new Subject(); + + store.select.mockReturnValue(selectionSubject); + store.dispatch = jest.fn(); + + facade = new NavigationFacade(useFromMock(<any>store)); + }) + + describe('getVorgangList', () => { + + it('should return selected value', (done) => { + const routeData: RouteData = createRouteData();; + + facade.getCurrentRouteData().subscribe(routeData => { + expect(routeData).toBe(routeData); + done(); + }); + + selectionSubject.next(routeData); + }); }) }) \ No newline at end of file diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.reducer.spec.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.reducer.spec.ts index e7304fe20bef43211e9c6d4edec7db82ae65f1f9..b1991393bbb82851a9ba6ca1e42b5267cc1015a4 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.reducer.spec.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.reducer.spec.ts @@ -1,4 +1,4 @@ -import { createCurrentRouteData } from './../../../test/navigation-test-factory'; +import { createRouteData } from './../../../test/navigation-test-factory'; import * as NavigationActions from './navigation.actions'; import { RouteData } from './navigation.models'; import { initialState, NavigationState, reducer } from './navigation.reducer'; @@ -8,12 +8,12 @@ describe('Navigation Reducer', () => { describe('on get updateRouteData action', () => { it('should set route data', () => { - const routeData: RouteData = createCurrentRouteData(); + const routeData: RouteData = createRouteData(); const action = NavigationActions.updateCurrentRouteData({ routeData }); const result: NavigationState = reducer(initialState, action); - expect(result.currentRouteData).toBe(routeData); + expect(result.currentRouteData).toStrictEqual(routeData); }) }) -}); +}); \ No newline at end of file diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.reducer.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.reducer.ts index 75aee5ca769f09b346319b0edea135e1418995da..ff4d0b847a17b7f9f5ce972df84732680552ba83 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.reducer.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.reducer.ts @@ -9,7 +9,7 @@ export interface NavigationPartialState { } export interface NavigationState { - currentRouteData: RouteData | null + currentRouteData: RouteData } export const initialState: NavigationState = { diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.selectors.spec.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.selectors.spec.ts index c55b63f00528a932323d197e82420aa1bca1723b..c33558007a575816153bbc0989e72de083e60df4 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.selectors.spec.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.selectors.spec.ts @@ -1,4 +1,4 @@ -import { createCurrentRouteData } from './../../../test/navigation-test-factory'; +import { createRouteData } from './../../../test/navigation-test-factory'; import { RouteData } from './navigation.models'; import { initialState, NavigationPartialState } from './navigation.reducer'; import * as NavigationSelectors from './navigation.selectors'; @@ -7,7 +7,7 @@ describe('Navigation Selectors', () => { let state: NavigationPartialState; - const currentRouteData: RouteData = createCurrentRouteData(); + const currentRouteData: RouteData = createRouteData(); beforeEach(() => { state = { diff --git a/goofy-client/libs/navigation-shared/test/navigation-test-factory.ts b/goofy-client/libs/navigation-shared/test/navigation-test-factory.ts index 41035fc36377bb313fdbb700b8289e5cc8fe8638..4dd7f7b1b415047dd226db3cb82c7ba43ada58db 100644 --- a/goofy-client/libs/navigation-shared/test/navigation-test-factory.ts +++ b/goofy-client/libs/navigation-shared/test/navigation-test-factory.ts @@ -1,5 +1,5 @@ import { RouteData } from '../src/lib/+state/navigation.models'; -export function createCurrentRouteData(): RouteData { +export function createRouteData(): RouteData { return { queryParameter: {}, urlSegments: [] }; } \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search-container.component.spec.ts b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search-container.component.spec.ts index b81d37953e31acc37ea85e421ce90cfaa624c813..59d5c0ba414032b07e7c29383a9d1df613f1cd47 100644 --- a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search-container.component.spec.ts +++ b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search-container.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ApiRootFacade, ApiRootLinkRel } from '@goofy-client/api-root-shared'; import { createStateResource, HasLinkPipe } from '@goofy-client/tech-shared'; import { mock } from '@goofy-client/test-utils'; -import { VorgangFacade } from '@goofy-client/vorgang-shared'; +import { VorgangListService } from '@goofy-client/vorgang-shared'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent } from 'ng-mocks'; @@ -16,7 +16,7 @@ describe('VorgangSearchContainerComponent', () => { const apiRootFacade = { ...mock(ApiRootFacade), getApiRoot: jest.fn() }; - const vorgangFacade = mock(VorgangFacade); + const vorgangListService = mock(VorgangListService); const vorgangSearch: string = getDataTestIdOf('vorgang-search'); @@ -33,8 +33,8 @@ describe('VorgangSearchContainerComponent', () => { useValue: apiRootFacade }, { - provide: VorgangFacade, - useValue: vorgangFacade + provide: VorgangListService, + useValue: vorgangListService } ] }).compileComponents(); @@ -61,7 +61,7 @@ describe('VorgangSearchContainerComponent', () => { it('should call vorgangFacade to get searchPreviewList', () => { component.ngOnInit(); - expect(vorgangFacade.getSearchPreviewList).toHaveBeenCalled(); + expect(vorgangListService.getSearchPreviewList).toHaveBeenCalled(); }) }) diff --git a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search-container.component.ts b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search-container.component.ts index 3a1d3d75acfe19dae20f1181eb488e445e72452a..def28f335093dbdd915c59849c4989bea46230a2 100644 --- a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search-container.component.ts +++ b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search-container.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { ApiRootFacade, ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-shared'; import { createEmptyStateResource, StateResource } from '@goofy-client/tech-shared'; -import { VorgangFacade, VorgangListResource } from '@goofy-client/vorgang-shared'; +import { VorgangListResource, VorgangListService } from '@goofy-client/vorgang-shared'; import { Observable, of } from 'rxjs'; @Component({ @@ -16,10 +16,10 @@ export class VorgangSearchContainerComponent implements OnInit { readonly apiRootLinkRel = ApiRootLinkRel; - constructor(private apiRootFacade: ApiRootFacade, private vorgangFacade: VorgangFacade) { } + constructor(private apiRootFacade: ApiRootFacade, private vorgangListService: VorgangListService) { } ngOnInit(): void { this.apiRoot$ = this.apiRootFacade.getApiRoot(); - this.vorgangSearchPreviewList$ = this.vorgangFacade.getSearchPreviewList(); + this.vorgangSearchPreviewList$ = this.vorgangListService.getSearchPreviewList(); } } \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.component.spec.ts b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.component.spec.ts index 7b80f57e842189d885629b8176bb9b37828f2c5a..30bdd79695bdb7aeec3542ee0a39a7501aa4efc8 100644 --- a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.component.spec.ts +++ b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.component.spec.ts @@ -9,7 +9,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { ConvertForDataTestPipe, createEmptyStateResource, createStateResource, EMPTY_STRING, HasLinkPipe, ToEmbeddedResourcesPipe } from '@goofy-client/tech-shared'; import { mock } from '@goofy-client/test-utils'; import { SpinnerComponent } from '@goofy-client/ui'; -import { SearchInfo, VorgangFacade, VorgangHeaderLinkRel } from '@goofy-client/vorgang-shared'; +import { SearchInfo, VorgangHeaderLinkRel, VorgangListService } from '@goofy-client/vorgang-shared'; import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; import { createVorgangListResource } from 'libs/vorgang-shared/test/vorgang'; import { MockComponent } from 'ng-mocks'; @@ -26,7 +26,7 @@ describe('VorgangSearchComponent', () => { const searchFormService = mock(VorgangSearchFormService); const searchInfoSubj: Subject<SearchInfo> = new BehaviorSubject({ searchString: EMPTY_STRING, changedAfterSearchDone: false }); - const vorgangFacade = { ...mock(VorgangFacade), getSearchInfo: () => searchInfoSubj }; + const vorgangListService = { ...mock(VorgangListService), getSearchInfo: () => searchInfoSubj }; const searchPreviewOption: string = getDataTestClassOf('search-preview-option'); @@ -57,8 +57,8 @@ describe('VorgangSearchComponent', () => { useValue: searchFormService }, { - provide: VorgangFacade, - useValue: vorgangFacade + provide: VorgangListService, + useValue: vorgangListService } ] }).compileComponents(); diff --git a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.formservice.spec.ts b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.formservice.spec.ts index 812e70712e5605381765a17a636a2f4b42334af1..2470d05df2b297a60eb3f07c234cf5ca082b24cd 100644 --- a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.formservice.spec.ts +++ b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.formservice.spec.ts @@ -3,7 +3,7 @@ import { Params } from '@angular/router'; import { faker } from '@faker-js/faker'; import { EMPTY_STRING, NavigationService } from '@goofy-client/tech-shared'; import { Mock, mock, useFromMock } from '@goofy-client/test-utils'; -import { VorgangFacade, VorgangHeaderLinkRel, VorgangResource } from '@goofy-client/vorgang-shared'; +import { VorgangHeaderLinkRel, VorgangListService, VorgangResource } from '@goofy-client/vorgang-shared'; import { getUrl, ResourceUri } from '@ngxp/rest'; import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; import { of } from 'rxjs'; @@ -12,16 +12,16 @@ import { VorgangSearchFormService } from './vorgang-search.formservice'; describe('VorgangSearchFormService', () => { let formService: VorgangSearchFormService;; let navigationService: Mock<NavigationService>; - let vorgangFacade: Mock<VorgangFacade> + let vorgangListService: Mock<VorgangListService> const SEARCH_STRING = 'i search for...'; beforeEach(() => { - vorgangFacade = mock(VorgangFacade); - vorgangFacade.getSearchInfo.mockReturnValue(of({})); + vorgangListService = mock(VorgangListService); + vorgangListService.getSearchInfo.mockReturnValue(of({})); navigationService = mock(NavigationService); - formService = new VorgangSearchFormService(new FormBuilder(), useFromMock(navigationService), useFromMock(vorgangFacade)); + formService = new VorgangSearchFormService(new FormBuilder(), useFromMock(navigationService), useFromMock(vorgangListService)); }) it('should create', () => { @@ -44,15 +44,31 @@ describe('VorgangSearchFormService', () => { formService.handleValueChanges(SEARCH_STRING); - expect(vorgangFacade.searchForPreview).toHaveBeenCalled(); + expect(vorgangListService.searchForPreview).toHaveBeenCalled(); }) - it('should clear preview list on 3 or less character', () => { + it('should not clear preview list on 2 character', () => { formService.searchLocked = false; formService.handleValueChanges('AH'); - expect(vorgangFacade.clearSearchPreviewList).toHaveBeenCalled(); + expect(vorgangListService.clearSearchPreviewList).not.toHaveBeenCalled(); + }) + + it('should clear preview list on null', () => { + formService.searchLocked = false; + + formService.handleValueChanges(null); + + expect(vorgangListService.clearSearchPreviewList).toHaveBeenCalled(); + }) + + it('should clear preview list on empty', () => { + formService.searchLocked = false; + + formService.handleValueChanges(EMPTY_STRING); + + expect(vorgangListService.clearSearchPreviewList).toHaveBeenCalled(); }) }) @@ -61,7 +77,7 @@ describe('VorgangSearchFormService', () => { it('should call submit for preview list', () => { formService.searchForPreviewList(SEARCH_STRING); - expect(vorgangFacade.searchForPreview).toHaveBeenCalledWith(SEARCH_STRING); + expect(vorgangListService.searchForPreview).toHaveBeenCalledWith(SEARCH_STRING); }) }) @@ -70,7 +86,7 @@ describe('VorgangSearchFormService', () => { it('should call submit for preview list', () => { formService.clearVorgangSearchPreviewList(); - expect(vorgangFacade.clearSearchPreviewList).toHaveBeenCalled(); + expect(vorgangListService.clearSearchPreviewList).toHaveBeenCalled(); }) }) @@ -122,7 +138,7 @@ describe('VorgangSearchFormService', () => { describe('patchSearchInfo', () => { - it('should patch search string field', () => { + it('should patch search string field if its different', () => { const newSearchString: string = 'i search for something other...'; getSearchFormControl().patchValue(SEARCH_STRING); diff --git a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.formservice.ts b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.formservice.ts index 245242decbc23c6f25c2ce46d2b0e9f6217a7ea0..745bcff45469fd6dc28daba04049b8500515a679 100644 --- a/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.formservice.ts +++ b/goofy-client/libs/vorgang-shared-ui/src/lib/vorgang-search-container/vorgang-search/vorgang-search.formservice.ts @@ -2,7 +2,7 @@ import { Injectable, OnDestroy } from '@angular/core'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Params } from '@angular/router'; import { EMPTY_STRING, hasMinLength, isNotEmpty, isNotNil, NavigationService, toResourceUri } from '@goofy-client/tech-shared'; -import { SearchInfo, VorgangFacade, VorgangHeaderLinkRel, VorgangResource } from '@goofy-client/vorgang-shared'; +import { SearchInfo, VorgangHeaderLinkRel, VorgangListService, VorgangResource } from '@goofy-client/vorgang-shared'; import { isEmpty } from 'lodash-es'; import { Subscription } from 'rxjs'; import { debounceTime, first } from 'rxjs/operators'; @@ -23,7 +23,7 @@ export class VorgangSearchFormService implements OnDestroy { constructor( private formBuilder: FormBuilder, private navigationService: NavigationService, - private vorgangFacade: VorgangFacade + private vorgangListService: VorgangListService ) { this.init(); } @@ -52,7 +52,7 @@ export class VorgangSearchFormService implements OnDestroy { } if (hasMinLength(searchString, this.PREVIEW_SEARCH_STRING_MIN_LENGTH)) { this.searchForPreviewList(searchString); - } else { + } else if (searchString == null || searchString == EMPTY_STRING) { this.clearVorgangSearchPreviewList(); } } @@ -62,20 +62,22 @@ export class VorgangSearchFormService implements OnDestroy { } searchForPreviewList(searchInput: string): void { - this.vorgangFacade.searchForPreview(searchInput); + this.vorgangListService.searchForPreview(searchInput); } clearVorgangSearchPreviewList(): void { - this.vorgangFacade.clearSearchPreviewList(); + this.vorgangListService.clearSearchPreviewList(); } private subscribeToSearchString(): void { - this.subscription = this.vorgangFacade.getSearchInfo().subscribe((searchInfo: SearchInfo) => this.patchSearchInfo(searchInfo)); + this.subscription = this.vorgangListService.getSearchInfo().subscribe((searchInfo: SearchInfo) => this.patchSearchInfo(searchInfo)); } patchSearchInfo(searchInfo: SearchInfo): void { - this.getSearchFormControl().patchValue(searchInfo.searchString); - + const searchStringInputValue: string = this.getSearchFormControl().value; + if (searchInfo.searchString != searchStringInputValue) { + this.getSearchFormControl().patchValue(searchInfo.searchString); + } this.updateSearchLock(searchInfo.changedAfterSearchDone); } @@ -102,7 +104,6 @@ export class VorgangSearchFormService implements OnDestroy { } submitByPreviewList(resource: VorgangResource): void { - //TODO: Suchstring im State leeren anstelle FormControl patchen. this.getSearchFormControl().patchValue(EMPTY_STRING); this.navigateToVorgang(resource); } diff --git a/goofy-client/libs/vorgang-shared/src/index.ts b/goofy-client/libs/vorgang-shared/src/index.ts index 4ffec6a8582d52998626df5f280289a7f0467fdd..d3cad9fd2daca488055be15aaba03d987bfad448 100644 --- a/goofy-client/libs/vorgang-shared/src/index.ts +++ b/goofy-client/libs/vorgang-shared/src/index.ts @@ -1,8 +1,5 @@ -export * from './lib/+state/vorgang.actions'; -export * from './lib/+state/vorgang.facade'; -export * from './lib/+state/vorgang.reducer'; -export * from './lib/+state/vorgang.selectors'; export * from './lib/vorgang-command.service'; +export * from './lib/vorgang-list.service'; export * from './lib/vorgang-shared.module'; export * from './lib/vorgang.linkrel'; export * from './lib/vorgang.messages'; diff --git a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.actions.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.actions.ts index 2a8c28177362700c416c17a7047c962742bc102c..943c904788224ca805828199234e50a3e4923c79 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.actions.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.actions.ts @@ -1,20 +1,49 @@ +import { ApiRootResource } from '@goofy-client/api-root-shared'; import { ApiError } from '@goofy-client/tech-shared'; -import { createAction, props } from '@ngrx/store'; +import { ActionCreator, createAction, props } from '@ngrx/store'; +import { TypedAction } from '@ngrx/store/src/models'; import { VorgangListResource } from '../vorgang.model'; -export const noOperation = createAction('[Vorgang-Routing] No Operation'); +export interface VorgangActionCreator<T> extends ActionCreator<string, (props: T) => T & TypedAction<string>> { } +export interface TypedActionCreator extends ActionCreator<string, () => TypedAction<string>> { } -export const loadVorgangList = createAction('[Vorgang] Load VorgangList'); -export const loadVorgangListSuccess = createAction('[Vorgang] Load VorgangList Success', props<{ loadedResource: VorgangListResource }>()); -export const loadVorgangListFailure = createAction('[Vorgang] Load VorgangList Failure', props<{ apiError: ApiError }>()); +export interface SearchVorgaengeByProps { + apiRoot: ApiRootResource, + searchString: string, + linkRel: string +} -export const loadNextPage = createAction('[Vorgang] Load next VorgangList page'); -export const reloadVorgangList = createAction('[Vorgang] Reload VorgangList', props<{ searchString: string }>()); +export interface StringBasedProps { + string: string +} -export const searchForPreview = createAction('[Vorgang] Search for preview', props<{ searchString: string }>()); -export const searchForPreviewSuccess = createAction('[Vorgang] Search for preview Success', props<{ loadedResource: VorgangListResource }>()); -export const searchForPreviewFailure = createAction('[Vorgang] Search for preview Failure', props<{ apiError: ApiError }>()); +export interface ApiRootAction { + apiRoot: ApiRootResource +} -export const clearSearchPreviewList = createAction('[Vorgang] Clear Search preview'); +export interface ApiErrorAction { + apiError: ApiError +} -export const setSearchString = createAction('[Vorgang] Set SearchString', props<{ searchString: string }>()); \ No newline at end of file +export interface VorgangListAction { + vorgangList: VorgangListResource +} + +export const noOperation: TypedActionCreator = createAction('[Vorgang-Routing] No Operation'); + +export const loadVorgangList: VorgangActionCreator<ApiRootAction> = createAction('[Vorgang] Load VorgangList', props<ApiRootAction>()); +export const searchVorgaengeBy: VorgangActionCreator<SearchVorgaengeByProps> = createAction('[Vorgang] Search VorgangList', props<SearchVorgaengeByProps>()); +export const searchVorgaengeBySuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Search VorgangList Success', props<VorgangListAction>()); + +export const loadMyVorgaengeList: VorgangActionCreator<ApiRootAction> = createAction('[Vorgang] Load MyVorgaengList', props<ApiRootAction>()); +export const loadVorgangListSuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Load VorgangList Success', props<VorgangListAction>()); +export const loadVorgangListFailure: VorgangActionCreator<ApiErrorAction> = createAction('[Vorgang] Load VorgangList Failure', props<ApiErrorAction>()); + +export const loadNextPage: TypedActionCreator = createAction('[Vorgang] Load next VorgangList page'); +export const loadNextPageSuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Load next VorgangList page Success', props<VorgangListAction>()); + +export const searchForPreview: VorgangActionCreator<StringBasedProps> = createAction('[Vorgang] Search for preview', props<StringBasedProps>()); +export const searchForPreviewSuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Search for preview Success', props<VorgangListAction>()); +export const searchForPreviewFailure: VorgangActionCreator<ApiErrorAction> = createAction('[Vorgang] Search for preview Failure', props<ApiErrorAction>()); + +export const clearSearchPreviewList: TypedActionCreator = createAction('[Vorgang] Clear Search preview'); \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.effects.spec.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.effects.spec.ts index 3d34c6cabbe02d2d11875d7bd203f2eaf0f3f21c..df3d8ba4cd22716a357be93bca5fe36f48bf07eb 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.effects.spec.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.effects.spec.ts @@ -1,11 +1,10 @@ import { TestBed } from '@angular/core/testing'; import { ApiRootFacade, ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-shared'; -import { NavigationFacade, RouteData } from '@goofy-client/navigation-shared'; -import { ApiError, createEmptyStateResource, createStateResource, StateResource } from '@goofy-client/tech-shared'; +import { NavigationFacade } from '@goofy-client/navigation-shared'; +import { ApiError, createStateResource } from '@goofy-client/tech-shared'; import { mock } from '@goofy-client/test-utils'; import { provideMockActions } from '@ngrx/effects/testing'; -import { routerNavigatedAction } from '@ngrx/router-store'; -import { Action, MemoizedSelector } from '@ngrx/store'; +import { Action } from '@ngrx/store'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { NxModule } from '@nrwl/angular'; import { cold, hot } from 'jest-marbles'; @@ -13,13 +12,11 @@ import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { createApiError } from 'libs/tech-shared/test/error'; import { createVorgangListResource } from 'libs/vorgang-shared/test/vorgang'; import { Observable, of } from 'rxjs'; -import { createCurrentRouteData } from '../../../../navigation-shared/test/navigation-test-factory'; -import * as NavigationUtil from '../vorgang-navigation.util'; +import { createRouteData } from '../../../../navigation-shared/test/navigation-test-factory'; import { VorgangListResource } from '../vorgang.model'; import { VorgangRepository } from '../vorgang.repository'; import * as VorgangActions from './vorgang.actions'; import { VorgangEffects } from './vorgang.effects'; -import { VorgangState } from './vorgang.reducer'; import * as VorgangSelectors from './vorgang.selectors'; describe('VorgangEffects', () => { @@ -33,8 +30,6 @@ describe('VorgangEffects', () => { const vorgangList: VorgangListResource = createVorgangListResource(); - let vorgangListSelector: MemoizedSelector<VorgangState, StateResource<VorgangListResource>>; - beforeEach(() => { TestBed.configureTestingModule({ imports: [NxModule.forRoot()], @@ -54,50 +49,35 @@ describe('VorgangEffects', () => { provide: NavigationFacade, useValue: navigationFacade } - ], + ] }); effects = TestBed.inject(VorgangEffects); store = TestBed.inject(MockStore); - vorgangListSelector = store.overrideSelector(VorgangSelectors.vorgangList, createStateResource(vorgangList)); + store.overrideSelector(VorgangSelectors.vorgangList, createStateResource(vorgangList)); }); - describe('loadVorgangList$', () => { + describe('loadVorgangList', () => { const vorgangList: VorgangListResource = createVorgangListResource(); const apiRoot: ApiRootResource = createApiRootResource(); - const currentRouteData: RouteData = createCurrentRouteData(); - - beforeEach(() => { - apiRootFacade.getApiRoot.mockReturnValue(of(createStateResource(apiRoot))); - navigationFacade.getCurrentRouteData.mockReturnValue(of(currentRouteData)); - - effects.loadVorgangList = jest.fn().mockReturnValue(of(vorgangList)); - }) - - it('should call api root facade', () => { - actions = of(VorgangActions.loadVorgangList()); - - effects.loadVorgangList$.subscribe(); + const action = VorgangActions.loadVorgangList({ apiRoot }); - expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); - }) - - it('should call loadVorgangList', () => { - actions = of(VorgangActions.loadVorgangList()); + it('should call repository', () => { + actions = of(action); effects.loadVorgangList$.subscribe(); - expect(effects.loadVorgangList).toHaveBeenCalledWith(apiRoot, currentRouteData); + expect(vorgangRepository.loadVorgangList).toHaveBeenCalledWith(apiRoot); }) it('should dispatch loadVorgangListSuccess action', () => { - effects.loadVorgangList = jest.fn().mockReturnValue(of(vorgangList)); + vorgangRepository.loadVorgangList.mockReturnValue(of(vorgangList)); - actions = hot('-a-|', { a: VorgangActions.loadVorgangList() }); + actions = hot('-a-|', { a: action }); - const expected = hot('-a-|', { a: VorgangActions.loadVorgangListSuccess({ loadedResource: vorgangList }) }); + const expected = hot('-a-|', { a: VorgangActions.loadVorgangListSuccess({ vorgangList }) }); expect(effects.loadVorgangList$).toBeObservable(expected); }) @@ -105,63 +85,90 @@ describe('VorgangEffects', () => { const apiError: ApiError = createApiError() const error = { error: { error: apiError } }; const errorResponse = cold('-#', {}, error); - effects.loadVorgangList = jest.fn(() => errorResponse); + vorgangRepository.loadVorgangList = jest.fn(() => errorResponse); - const expected = cold('--c', { c: VorgangActions.loadVorgangListFailure({ apiError }) }); - actions = hot('-a', { a: VorgangActions.loadVorgangList() }); + const expected = cold('--b', { b: VorgangActions.loadVorgangListFailure({ apiError }) }); + actions = hot('-a', { a: action }); expect(effects.loadVorgangList$).toBeObservable(expected); }) }) - describe('loadVorgangList', () => { + describe('searchVorgaengeBy', () => { + const vorgangList: VorgangListResource = createVorgangListResource(); const apiRoot: ApiRootResource = createApiRootResource(); - const searchString: string = 'searchThisForMe' - const searchLinkRel: ApiRootLinkRel = ApiRootLinkRel.SEARCH; + const searchString: string = 'search like me'; + const linkRel: string = 'linkRelationName'; + const action = VorgangActions.searchVorgaengeBy({ apiRoot, searchString, linkRel }); + + it('should call repository', () => { + actions = of(action); + + effects.searchVorgaengeBy$.subscribe(); + + expect(vorgangRepository.searchVorgaengeBy).toHaveBeenCalledWith(apiRoot, searchString, linkRel); + }) + it('should dispatch searchVorgaengeBySuccess action', () => { + vorgangRepository.searchVorgaengeBy.mockReturnValue(of(vorgangList)); - describe('with existing searchString', () => { + actions = hot('-a-|', { a: action }); - beforeEach(() => { - jest.spyOn(NavigationUtil, 'existSearchString').mockReturnValue(true); - }) + const expected = hot('-a-|', { a: VorgangActions.searchVorgaengeBySuccess({ vorgangList }) }); + expect(effects.searchVorgaengeBy$).toBeObservable(expected); + }) - it('should call repository searchVorgaengeBy', () => { - jest.spyOn(NavigationUtil, 'getSearchString').mockReturnValue(searchString); - jest.spyOn(NavigationUtil, 'getSearchLinkRel').mockReturnValue(searchLinkRel); + it('should dispatch loadVorgangListFailure action', () => { + const apiError: ApiError = createApiError() + const error = { error: { error: apiError } }; + const errorResponse = cold('-#', {}, error); + vorgangRepository.searchVorgaengeBy = jest.fn(() => errorResponse); - effects.loadVorgangList(apiRoot, createCurrentRouteData()); + const expected = cold('--b', { b: VorgangActions.loadVorgangListFailure({ apiError }) }); + actions = hot('-a', { a: action }); - expect(vorgangRepository.searchVorgaengeBy).toHaveBeenCalledWith(apiRoot, searchString, searchLinkRel); - }) + expect(effects.searchVorgaengeBy$).toBeObservable(expected); }) + }) - describe('without searchString', () => { + describe('loadMyVorgaengeList', () => { - beforeEach(() => { - jest.spyOn(NavigationUtil, 'existSearchString').mockReturnValue(false); - }) + const vorgangList: VorgangListResource = createVorgangListResource(); + const apiRoot: ApiRootResource = createApiRootResource(); + const action = VorgangActions.loadMyVorgaengeList({ apiRoot }); + + it('should call repository', () => { + actions = of(action); - it('should call repository loadMyVorgaengeList if urlSegement exists', () => { - jest.spyOn(NavigationUtil, 'isMyVorgaenge').mockReturnValue(true); + effects.loadMyVorgaengeList$.subscribe(); - effects.loadVorgangList(apiRoot, createCurrentRouteData()); + expect(vorgangRepository.loadMyVorgaengeList).toHaveBeenCalledWith(apiRoot); + }) - expect(vorgangRepository.loadMyVorgaengeList).toHaveBeenCalledWith(apiRoot); - }) + it('should dispatch loadVorgangListSuccess action', () => { + vorgangRepository.loadMyVorgaengeList.mockReturnValue(of(vorgangList)); - it('should call repository loadVorgangList', () => { - jest.spyOn(NavigationUtil, 'isMyVorgaenge').mockReturnValue(false); + actions = hot('-a-|', { a: action }); + + const expected = hot('-a-|', { a: VorgangActions.loadVorgangListSuccess({ vorgangList }) }); + expect(effects.loadMyVorgaengeList$).toBeObservable(expected); + }) + + it('should dispatch loadVorgangListFailure action', () => { + const apiError: ApiError = createApiError() + const error = { error: { error: apiError } }; + const errorResponse = cold('-#', {}, error); + vorgangRepository.loadMyVorgaengeList = jest.fn(() => errorResponse); - effects.loadVorgangList(apiRoot, createCurrentRouteData()); + const expected = cold('--b', { b: VorgangActions.loadVorgangListFailure({ apiError }) }); + actions = hot('-a', { a: action }); - expect(vorgangRepository.loadVorgangList).toHaveBeenCalledWith(apiRoot); - }) + expect(effects.loadMyVorgaengeList$).toBeObservable(expected); }) }) - describe('loadNextPage$', () => { + describe('loadNextPage', () => { const action = VorgangActions.loadNextPage(); @@ -187,7 +194,7 @@ describe('VorgangEffects', () => { actions = hot('-a-|', { a: action }); - const expected = hot('-a-|', { a: VorgangActions.loadVorgangListSuccess({ loadedResource: vorgangList }) }); + const expected = hot('-a-|', { a: VorgangActions.loadNextPageSuccess({ vorgangList }) }); expect(effects.loadNextPage$).toBeObservable(expected); }) @@ -204,17 +211,17 @@ describe('VorgangEffects', () => { }) }) - describe('searchForPreview$', () => { + describe('searchForPreview', () => { const vorgangList: VorgangListResource = createVorgangListResource(); const apiRoot: ApiRootResource = createApiRootResource(); const searchString: string = 'searchThisForMe'; - const action = VorgangActions.searchForPreview({ searchString }); + const action = VorgangActions.searchForPreview({ string: searchString }); beforeEach(() => { apiRootFacade.getApiRoot.mockReturnValue(of(createStateResource(apiRoot))); - navigationFacade.getCurrentRouteData.mockReturnValue(of(createCurrentRouteData())); + navigationFacade.getCurrentRouteData.mockReturnValue(of(createRouteData())); }) it('should call apiRootFacade', () => { @@ -246,7 +253,7 @@ describe('VorgangEffects', () => { actions = hot('-a-|', { a: action }); - const expected = hot('-a-|', { a: VorgangActions.searchForPreviewSuccess({ loadedResource: vorgangList }) }); + const expected = hot('-a-|', { a: VorgangActions.searchForPreviewSuccess({ vorgangList }) }); expect(effects.searchForPreview$).toBeObservable(expected); }) @@ -262,86 +269,4 @@ describe('VorgangEffects', () => { expect(effects.searchForPreview$).toBeObservable(expected); }) }) - - describe('navigate$', () => { - - const action = routerNavigatedAction; - const currentRouteData: RouteData = createCurrentRouteData(); - - beforeEach(() => { - navigationFacade.getCurrentRouteData.mockReturnValue(of(currentRouteData)); - }) - - it('should select vorgangList from state', () => { - store.select = jest.fn(); - actions = of(action); - - effects.navigate$.subscribe(); - - expect(store.select).toHaveBeenCalledWith(VorgangSelectors.vorgangList); - }) - - it('should call navigationFacade', () => { - actions = of(action); - - effects.navigate$.subscribe(); - - expect(navigationFacade.getCurrentRouteData).toHaveBeenCalled(); - }) - - it('should dispatch noOperation action', () => { - vorgangListSelector.setResult(createEmptyStateResource()); - actions = hot('-a-|', { a: action }); - - effects.navigate$.subscribe(); - - const expected = hot('-a-|', { a: VorgangActions.noOperation() }); - expect(effects.navigate$).toBeObservable(expected); - }) - - it('should dispatch reloadVorgang action', () => { - vorgangListSelector.setResult(createStateResource(vorgangList)); - jest.spyOn(NavigationUtil, 'navigateFromVorgangList').mockReturnValue(true); - - actions = hot('-a-|', { a: action }); - - effects.navigate$.subscribe(); - - const expected = hot('-a-|', { a: VorgangActions.reloadVorgangList({ searchString: null }) }); - expect(effects.navigate$).toBeObservable(expected); - }) - - describe('with existing searchString', () => { - - const searchString: string = 'searchThisForMe'; - - beforeEach(() => { - jest.spyOn(NavigationUtil, 'getSearchString').mockReturnValue(searchString); - jest.spyOn(NavigationUtil, 'existSearchString').mockReturnValue(true); - }) - - it('should dispatch reloadVorgang with searchString action', () => { - vorgangListSelector.setResult(createStateResource(vorgangList)); - jest.spyOn(NavigationUtil, 'navigateFromVorgangList').mockReturnValue(false); - - actions = hot('-a-|', { a: action }); - - effects.navigate$.subscribe(); - - const expected = hot('-a-|', { a: VorgangActions.reloadVorgangList({ searchString }) }); - expect(effects.navigate$).toBeObservable(expected); - }) - - it('should dispatch setSearchString action', () => { - vorgangListSelector.setResult(createEmptyStateResource()); - - actions = hot('-a-|', { a: action }); - - effects.navigate$.subscribe(); - - const expected = hot('-a-|', { a: VorgangActions.setSearchString({ searchString }) }); - expect(effects.navigate$).toBeObservable(expected); - }) - }) - }) }); \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.effects.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.effects.ts index e7bf7f4e353b70ea4f91f50297686ac97e746d08..899ee49b85c033d0ad671895483e6d739765a697 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.effects.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.effects.ts @@ -1,15 +1,15 @@ import { Injectable } from '@angular/core'; -import { ApiRootFacade, ApiRootResource } from '@goofy-client/api-root-shared'; -import { NavigationFacade, RouteData } from '@goofy-client/navigation-shared'; +import { ApiRootFacade } from '@goofy-client/api-root-shared'; +import { NavigationFacade } from '@goofy-client/navigation-shared'; +import { ApiError } from '@goofy-client/tech-shared'; import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; -import { routerNavigatedAction } from '@ngrx/router-store'; import { Store } from '@ngrx/store'; -import { Observable, of } from 'rxjs'; +import { of } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; -import { existSearchString, getSearchLinkRel, getSearchString, isMyVorgaenge, navigateFromVorgangList } from '../vorgang-navigation.util'; -import { VorgangListResource } from '../vorgang.model'; +import { getSearchLinkRel } from '../vorgang-navigation.util'; import { VorgangRepository } from '../vorgang.repository'; import * as VorgangActions from './vorgang.actions'; +import { ApiRootAction, SearchVorgaengeByProps } from './vorgang.actions'; import * as VorgangSelectors from './vorgang.selectors'; @Injectable() @@ -24,36 +24,41 @@ export class VorgangEffects { loadVorgangList$ = createEffect(() => this.actions$.pipe( ofType(VorgangActions.loadVorgangList), - concatLatestFrom(() => [this.apiRootFacade.getApiRoot(), this.navigationFacade.getCurrentRouteData()]), - switchMap(([, apiRoot, currentRouteData]) => { - return this.loadVorgangList(apiRoot.resource, currentRouteData).pipe( - map(listResource => VorgangActions.loadVorgangListSuccess({ loadedResource: listResource })), - catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) }))) - ); - }) + switchMap((action: ApiRootAction) => this.repository.loadVorgangList(action.apiRoot).pipe( + map(loadedVorgangList => VorgangActions.loadVorgangListSuccess({ vorgangList: loadedVorgangList })), + catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) }))) + )) ) ) - loadVorgangList(apiRoot: ApiRootResource, currentRouteData: RouteData): Observable<VorgangListResource> { - if (existSearchString(currentRouteData)) { - return this.repository.searchVorgaengeBy(apiRoot, getSearchString(currentRouteData), getSearchLinkRel(currentRouteData)); - } - if (isMyVorgaenge(currentRouteData)) { - return this.repository.loadMyVorgaengeList(apiRoot); - } - return this.repository.loadVorgangList(apiRoot); - } + searchVorgaengeBy$ = createEffect(() => + this.actions$.pipe( + ofType(VorgangActions.searchVorgaengeBy), + switchMap((action: SearchVorgaengeByProps) => this.repository.searchVorgaengeBy(action.apiRoot, action.searchString, action.linkRel).pipe( + map(loadedVorgangList => VorgangActions.searchVorgaengeBySuccess({ vorgangList: loadedVorgangList })), + catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) }))) + )) + ) + ) + + loadMyVorgaengeList$ = createEffect(() => + this.actions$.pipe( + ofType(VorgangActions.loadMyVorgaengeList), + switchMap((action: ApiRootAction) => this.repository.loadMyVorgaengeList(action.apiRoot).pipe( + map(loadedVorgangList => VorgangActions.loadVorgangListSuccess({ vorgangList: loadedVorgangList })), + catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) }))) + )) + ) + ) loadNextPage$ = createEffect(() => this.actions$.pipe( ofType(VorgangActions.loadNextPage), concatLatestFrom(() => this.store.select(VorgangSelectors.vorgangList)), - switchMap(([, vorgangList]) => { - return this.repository.getNextVorgangListPage(vorgangList.resource).pipe( - map(listResource => VorgangActions.loadVorgangListSuccess({ loadedResource: listResource })), - catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) }))) - ) - }) + switchMap(([, vorgangList]) => this.repository.getNextVorgangListPage(vorgangList.resource).pipe( + map(loadedVorgangList => VorgangActions.loadNextPageSuccess({ vorgangList: loadedVorgangList })), + catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) }))) + )) ) ) @@ -61,37 +66,16 @@ export class VorgangEffects { this.actions$.pipe( ofType(VorgangActions.searchForPreview), concatLatestFrom(() => [this.apiRootFacade.getApiRoot(), this.navigationFacade.getCurrentRouteData()]), - switchMap(([action, apiRoot, currentRouteData]) => { - return this.repository.searchVorgaengeBy(apiRoot.resource, action.searchString, getSearchLinkRel(currentRouteData), VorgangEffects.SEARCH_PREVIEW_LIST_LIMIT).pipe( - map(listResource => VorgangActions.searchForPreviewSuccess({ loadedResource: listResource })), + switchMap(([stringBasedProps, apiRoot, currentRouteData]) => { + return this.repository.searchVorgaengeBy(apiRoot.resource, stringBasedProps.string, getSearchLinkRel(currentRouteData), VorgangEffects.SEARCH_PREVIEW_LIST_LIMIT).pipe( + map(loadedVorgangList => VorgangActions.searchForPreviewSuccess({ vorgangList: loadedVorgangList })), catchError(error => of(VorgangActions.searchForPreviewFailure({ apiError: this.getApiErrorFromHttpError(error) }))) ) }) ) ) - private getApiErrorFromHttpError(error: any) { + private getApiErrorFromHttpError(error: any): ApiError { return error.error.error; } - - navigate$ = createEffect(() => - this.actions$.pipe( - ofType(routerNavigatedAction), - concatLatestFrom(() => [this.store.select(VorgangSelectors.vorgangList), this.navigationFacade.getCurrentRouteData()]), - switchMap(([, vorgangList, currentRouteData]) => { - if (vorgangList.loaded) { - if (navigateFromVorgangList(currentRouteData)) { - return of(VorgangActions.reloadVorgangList({ searchString: null })); - } - if (existSearchString(currentRouteData)) { - return of(VorgangActions.reloadVorgangList({ searchString: getSearchString(currentRouteData) })); - } - } - if (existSearchString(currentRouteData)) { - return of(VorgangActions.setSearchString({ searchString: getSearchString(currentRouteData) })); - } - return of(VorgangActions.noOperation()); - }) - ) - ) } \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.facade.spec.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.facade.spec.ts index 0c35a5c4bc9dc878e15123683ae30096a99c264b..f8b7534f40aa7ed0d1b4eff28b6934609d3f4306 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.facade.spec.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.facade.spec.ts @@ -1,6 +1,8 @@ -import { createEmptyStateResource, createStateResource, EMPTY_STRING, StateResource } from '@goofy-client/tech-shared'; +import { ApiRootResource } from '@goofy-client/api-root-shared'; +import { createStateResource, EMPTY_STRING, StateResource } from '@goofy-client/tech-shared'; import { Mock, mock, useFromMock } from '@goofy-client/test-utils'; import { Store } from '@ngrx/store'; +import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { createVorgangListResource, createVorgangResources } from 'libs/vorgang-shared/test/vorgang'; import { Subject } from 'rxjs'; import { SearchInfo, VorgangListResource, VorgangResource } from '../vorgang.model'; @@ -30,55 +32,51 @@ describe('VorgangFacade', () => { describe('getVorgangList', () => { - describe('on loaded resource', () => { + it('should return selected value', (done) => { + const vorgaengeStateResource: StateResource<VorgangResource[]> = createStateResource(createVorgangResources()); - it('should return selected value', (done) => { - const vorgangListStateResource: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + facade.getVorgangList().subscribe(vorgaenge => { + expect(vorgaenge).toBe(vorgaengeStateResource); + done(); + }); - facade.getVorgangList().subscribe(vorgangList => { - expect(store.dispatch).not.toHaveBeenCalledWith(VorgangActions.loadVorgangList()); - expect(vorgangList).toBe(vorgangListStateResource); - done(); - }); + selectionSubject.next(vorgaengeStateResource); + }); + }) - selectionSubject.next(vorgangListStateResource); - }); + describe('loadVorgangList', () => { - it('should not dispatch action', (done) => { - const vorgangListStateResource: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + it('should dispatch "loadVorgangList" action', () => { + const apiRoot: ApiRootResource = createApiRootResource(); - facade.getVorgangList().subscribe(() => { - expect(store.dispatch).not.toHaveBeenCalledWith(VorgangActions.loadVorgangList()); - done(); - }); + facade.loadVorgangList(apiRoot); - selectionSubject.next(vorgangListStateResource); - }) - }) + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.loadVorgangList({ apiRoot })); + }); + }) - describe('on NOT loaded resource', () => { + describe('searchVorgaengeBy', () => { - it('should set loading to true', (done) => { - facade.getVorgangList().subscribe(historieList => { - expect(historieList.loading).toBe(true); - done(); - }); + const apiRoot: ApiRootResource = createApiRootResource(); + const searchString: string = 'search like me'; + const linkRel: string = 'LinkRelationName'; - selectionSubject.next(createEmptyStateResource()); - selectionSubject.next(createEmptyStateResource(true)); - }); + it('should dispatch "searchVorgaengeBy" action', () => { + facade.searchVorgaengeBy(apiRoot, searchString, linkRel); - it('should dispatch "loadVorgangList" action if not loaded', (done) => { - facade.getVorgangList().subscribe(historieList => { - expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.loadVorgangList()); - expect(historieList.loading).toBe(true); - done(); - }); + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.searchVorgaengeBy({ apiRoot, searchString, linkRel })); + }); + }) - selectionSubject.next(createEmptyStateResource()); - selectionSubject.next(createEmptyStateResource(true)); - }); - }) + describe('loadMyVorgaengeList', () => { + + it('should dispatch "loadMyVorgaengeList" action', () => { + const apiRoot: ApiRootResource = createApiRootResource(); + + facade.loadMyVorgaengeList(apiRoot); + + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.loadMyVorgaengeList({ apiRoot })); + }); }) describe('getVorgaenge', () => { @@ -95,7 +93,7 @@ describe('VorgangFacade', () => { }); }) - describe('getSearchString', () => { + describe('getSearchInfo', () => { it('should return selected value', (done) => { const searchInfoStateValue: StateResource<SearchInfo> = createStateResource({ changedAfterSearchDone: false, searchString: EMPTY_STRING }); @@ -123,7 +121,7 @@ describe('VorgangFacade', () => { it('should dispatch "searchForPreview" action', () => { facade.searchForPreview('searchThisForMe'); - expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.searchForPreview({ searchString: 'searchThisForMe' })); + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.searchForPreview({ string: 'searchThisForMe' })); }); }) diff --git a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.facade.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.facade.ts index a6fcf58ae8e57f409dca28152410f37816c6015b..9a6c674019f4aa856c0c7445ffaecbb056576a3f 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.facade.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.facade.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; -import { doIfLoadingRequired, StateResource } from '@goofy-client/tech-shared'; +import { ApiRootResource } from '@goofy-client/api-root-shared'; +import { StateResource } from '@goofy-client/tech-shared'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { filter } from 'rxjs/operators'; import { SearchInfo, VorgangListResource, VorgangResource } from '../vorgang.model'; import * as VorgangActions from './vorgang.actions'; import * as VorgangSelectors from './vorgang.selectors'; @@ -13,8 +13,19 @@ export class VorgangFacade { constructor(private readonly store: Store) { } public getVorgangList(): Observable<StateResource<VorgangListResource>> { - return this.store.select(VorgangSelectors.vorgangList).pipe( - filter(vorgangList => !doIfLoadingRequired(vorgangList, () => this.store.dispatch(VorgangActions.loadVorgangList())))); + return this.store.select(VorgangSelectors.vorgangList); + } + + public loadVorgangList(apiRoot: ApiRootResource): void { + this.store.dispatch(VorgangActions.loadVorgangList({ apiRoot })); + } + + public searchVorgaengeBy(apiRoot: ApiRootResource, searchString: string, linkRel: string): void { + this.store.dispatch(VorgangActions.searchVorgaengeBy({ apiRoot, searchString, linkRel })); + } + + public loadMyVorgaengeList(apiRoot: ApiRootResource): void { + this.store.dispatch(VorgangActions.loadMyVorgaengeList({ apiRoot })); } public getVorgaenge(): Observable<VorgangResource[]> { @@ -30,7 +41,7 @@ export class VorgangFacade { } public searchForPreview(searchString: string): void { - this.store.dispatch(VorgangActions.searchForPreview({ searchString })); + this.store.dispatch(VorgangActions.searchForPreview({ string: searchString })); } public getSearchPreviewList(): Observable<StateResource<VorgangListResource>> { diff --git a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts index ad50201cf3aed9668b9ad8374c22696b0524a996..239f475a99a39157df9ed1b1174f4b0b6ad724ef 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts @@ -1,10 +1,18 @@ +import { UrlSegment } from '@angular/router'; +import { ApiRootResource } from '@goofy-client/api-root-shared'; +import { RouteData } from '@goofy-client/navigation-shared'; import { ApiError, createEmptyStateResource, createStateResource, EMPTY_ARRAY } from '@goofy-client/tech-shared'; import { Action } from '@ngrx/store'; -import { createVorgangListResource, createVorgangListResourceWithResource, createVorgangResources } from 'libs/vorgang-shared/test/vorgang'; +import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; +import { createRouteData } from 'libs/navigation-shared/test/navigation-test-factory'; +import { createVorgangListResource, createVorgangListResourceWithResource, createVorgangResource, createVorgangResources } from 'libs/vorgang-shared/test/vorgang'; +import * as NavigationActions from '../../../../navigation-shared/src/lib/+state/navigation.actions'; import { createApiError } from '../../../../tech-shared/test/error'; +import * as VorgangNavigationUtil from '../vorgang-navigation.util'; import { VorgangListLinkRel } from '../vorgang.linkrel'; import { VorgangListResource, VorgangResource } from '../vorgang.model'; import * as VorgangActions from './vorgang.actions'; +import { VorgangEffects } from './vorgang.effects'; import { initialState, reducer, VorgangState } from './vorgang.reducer'; describe('Vorgang Reducer', () => { @@ -25,7 +33,18 @@ describe('Vorgang Reducer', () => { describe('on "loadVorgangList" action', () => { it('should set loading to true', () => { - const action = VorgangActions.loadVorgangList(); + const action = VorgangActions.loadVorgangList({ apiRoot: createApiRootResource() }); + + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList.loading).toBeTruthy(); + }) + }) + + describe('on "loadMyVorgaengeList" action', () => { + + it('should set loading to true', () => { + const action = VorgangActions.loadMyVorgaengeList({ apiRoot: createApiRootResource() }); const state: VorgangState = reducer(initialState, action); @@ -36,13 +55,13 @@ describe('Vorgang Reducer', () => { describe('on "loadVorgangListSuccess" action', () => { const vorgaenge: VorgangResource[] = createVorgangResources(); - const vorgangListResource: VorgangListResource = createVorgangListResourceWithResource(vorgaenge, [VorgangListLinkRel.NEXT]); - const action = VorgangActions.loadVorgangListSuccess({ loadedResource: vorgangListResource }); + const vorgangList: VorgangListResource = createVorgangListResourceWithResource(vorgaenge, [VorgangListLinkRel.NEXT]); + const action = VorgangActions.loadVorgangListSuccess({ vorgangList }); it('should set loaded resource', () => { const state: VorgangState = reducer(initialState, action); - expect(state.vorgangList).toEqual(createStateResource(vorgangListResource)); + expect(state.vorgangList).toEqual(createStateResource(vorgangList)); }) it('should set vorgaenge', () => { @@ -67,7 +86,7 @@ describe('Vorgang Reducer', () => { const state: VorgangState = reducer(initialState, action); - expect(state.vorgangList.error).toBe(apiError); + expect(state.vorgangList.error).toStrictEqual(apiError); }) it('should clear searchPreviewList', () => { @@ -81,39 +100,94 @@ describe('Vorgang Reducer', () => { }) }) - describe('on "reloadVorgangList" action', () => { + describe('loadNextPage', () => { - const searchString: string = 'searchThisForMe'; - const action = VorgangActions.reloadVorgangList({ searchString }); + describe('on "loadNextPage" action', () => { - it('should set vorgangList to reload', () => { - const state: VorgangState = reducer(initialState, action); + it('should set vorgangList reload to true', () => { + const action = VorgangActions.loadNextPage(); + + const state: VorgangState = reducer(initialState, action); - expect(state.vorgangList.reload).toBeTruthy(); + expect(state.vorgangList.reload).toBeTruthy(); + }) }) - it('should clear vorgaenge', () => { - const state: VorgangState = reducer({ ...initialState, vorgaenge: createVorgangResources() }, action); + describe('on "loadNextPageSuccess" action', () => { - expect(state.vorgaenge).toBe(EMPTY_ARRAY); - }) + const vorgangList: VorgangListResource = createVorgangListResource(); - it('should set searchString', () => { - const state: VorgangState = reducer(initialState, action); + it('should set vorgangList', () => { + const action = VorgangActions.loadNextPageSuccess({ vorgangList }); - expect(state.searchInfo.searchString).toBe(searchString); + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList).toStrictEqual(createStateResource(vorgangList)); + }) + + it('should add vorgaenge', () => { + const action = VorgangActions.loadNextPageSuccess({ vorgangList }); + + const state: VorgangState = reducer({ ...initialState, vorgaenge: [createVorgangResource()] }, action); + + expect(state.vorgaenge.length).toBe(11); + }) }) }) - describe('on "setSearchString" action', () => { + describe('searchVorgaengeBy', () => { + + describe('on "searchVorgaengeBy" action', () => { + + const apiRoot: ApiRootResource = createApiRootResource(); + const searchString: string = 'search like me'; + const linkRel: string = 'LinkRelationName'; + + const action = VorgangActions.searchVorgaengeBy({ apiRoot, searchString, linkRel }); + + it('should vorgangList loading', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList.loading).toBeTruthy(); + }) + + it('should clear vorgaenge', () => { + const state: VorgangState = reducer(initialState, action); - const searchString: string = 'searchThisForMe'; - const action = VorgangActions.setSearchString({ searchString }); + expect(state.vorgaenge).toBe(EMPTY_ARRAY); + }) - it('should set searchString', () => { - const state: VorgangState = reducer(initialState, action); + it('should clear searchPreviewList', () => { + const state: VorgangState = reducer(initialState, action); - expect(state.searchInfo.searchString).toBe(searchString); + expect(state.searchPreviewList).toStrictEqual(createEmptyStateResource()); + }) + }) + + describe('on "searchVorgaengeBySuccess" action', () => { + + const vorgaenge: VorgangResource[] = createVorgangResources(); + const vorgangList: VorgangListResource = createVorgangListResourceWithResource(vorgaenge); + + const action = VorgangActions.searchVorgaengeBySuccess({ vorgangList }); + + it('should set vorgangList', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList).toStrictEqual(createStateResource(vorgangList)); + }) + + it('should set vorgaenge', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgaenge).toBe(vorgaenge); + }) + + it('should clear searchPreviewList', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.searchPreviewList).toStrictEqual(createEmptyStateResource()); + }) }) }) @@ -122,7 +196,7 @@ describe('Vorgang Reducer', () => { describe('on "searchForPreview" action', () => { const searchString: string = 'searchThisForMe'; - const action = VorgangActions.searchForPreview({ searchString }); + const action = VorgangActions.searchForPreview({ string: searchString }); it('should set loading to true', () => { const state: VorgangState = reducer(initialState, action); @@ -133,13 +207,13 @@ describe('Vorgang Reducer', () => { describe('on "searchForPreviewSuccess" action', () => { - const vorgangListResource: VorgangListResource = createVorgangListResource(); - const action = VorgangActions.searchForPreviewSuccess({ loadedResource: vorgangListResource }); + const vorgangList: VorgangListResource = createVorgangListResource(); + const action = VorgangActions.searchForPreviewSuccess({ vorgangList }); it('should set loading to true', () => { const state: VorgangState = reducer(initialState, action); - expect(state.searchPreviewList.resource).toBe(vorgangListResource); + expect(state.searchPreviewList.resource).toStrictEqual(vorgangList); }) }) @@ -151,7 +225,7 @@ describe('Vorgang Reducer', () => { it('should set error', () => { const state: VorgangState = reducer(initialState, action); - expect(state.searchPreviewList.error).toBe(apiError); + expect(state.searchPreviewList.error).toStrictEqual(apiError); }) }) @@ -166,4 +240,99 @@ describe('Vorgang Reducer', () => { }) }) }) + + describe('on "updateCurrentReouteData" action', () => { + + const routeData: RouteData = createRouteData(); + + describe('navigate to "myVorgaenge"', () => { + + const action = NavigationActions.updateCurrentRouteData({ routeData }); + + beforeEach(() => { + jest.spyOn(VorgangNavigationUtil, 'isMyVorgaenge').mockReturnValue(true); + }) + + it('should set searchInfo', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.searchInfo.searchString).toEqual(null); + }) + + it.skip('should set vorganglist reload to true', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList.reload).toBeTruthy(); + }) + }) + + describe('navigate to vorgangList page', () => { + + const action = NavigationActions.updateCurrentRouteData({ routeData }); + + beforeEach(() => { + jest.spyOn(VorgangNavigationUtil, 'isMyVorgaenge').mockReturnValue(false); + jest.spyOn(VorgangNavigationUtil, 'isVorgangListPage').mockReturnValue(true); + }) + + it('should set searchInfo', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.searchInfo.searchString).toEqual(null); + }) + }) + + describe('navigate with existing search string', () => { + + const searchString: string = 'search like me'; + const action = NavigationActions.updateCurrentRouteData({ routeData: buildCurrentRouteData(searchString) }); + + it('should set searchInfo', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.searchInfo.searchString).toEqual(searchString); + }) + + it.skip('should vorgangList reload to true', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList.reload).toBeTruthy(); + }) + + it('should clear searchPreviewList on changed searchString', () => { + const state: VorgangState = reducer({ ...initialState, searchInfo: { ...initialState.searchInfo, searchString: 'existingSearchString' } }, action); + + expect(state.searchPreviewList).toStrictEqual(createEmptyStateResource()); + }) + }) + + describe('navigate to vorgangDetailPage', () => { + + const action = NavigationActions.updateCurrentRouteData({ routeData: { ...routeData, queryParameter: { ['vorgangWithEingangUrl']: 'encodedVorgangUri' } } }); + + it('should set searchInfo', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.searchInfo.searchString).toBeNull(); + }) + + it.skip('should set vorganglist reload to true', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList.reload).toBeTruthy(); + }) + + it('should clear vorgaenge', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgaenge).toBe(EMPTY_ARRAY); + }) + }) + + function buildCurrentRouteData(searchString: string): RouteData { + const urlSegments: UrlSegment[] = [<any>{ path: VorgangEffects.SEARCH_QUERY_PARAM }, <any>{ path: searchString }]; + const queryParameter = { [VorgangEffects.SEARCH_QUERY_PARAM]: searchString }; + return { ...createRouteData(), urlSegments, queryParameter }; + } + }) }); \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts index 4c8f5560114a7491ba35511f182d79e249b40ccf..e98402f9fdc8209406f8253b40c9723c9d08004e 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts @@ -1,8 +1,12 @@ -import { createEmptyStateResource, createErrorStateResource, createStateResource, EMPTY_ARRAY, EMPTY_STRING, StateResource } from '@goofy-client/tech-shared'; +import { RouteData } from '@goofy-client/navigation-shared'; +import { createEmptyStateResource, createErrorStateResource, createStateResource, EMPTY_ARRAY, StateResource } from '@goofy-client/tech-shared'; import { Action, createReducer, on } from '@ngrx/store'; +import * as NavigationActions from '../../../../navigation-shared/src/lib/+state/navigation.actions'; +import { getSearchString, isMyVorgaenge, isSearch, isVorgangDetailPage, isVorgangListPage } from '../vorgang-navigation.util'; import { SearchInfo, VorgangListResource, VorgangResource } from '../vorgang.model'; import { getVorgaengeFromList } from '../vorgang.util'; import * as VorgangActions from './vorgang.actions'; +import { ApiErrorAction, VorgangListAction } from './vorgang.actions'; export const VORGANG_FEATURE_KEY = 'VorgangState'; @@ -20,60 +24,125 @@ export interface VorgangState { export const initialState: VorgangState = { vorgangList: createEmptyStateResource(), vorgaenge: EMPTY_ARRAY, - searchInfo: { searchString: EMPTY_STRING, changedAfterSearchDone: false }, + searchInfo: { searchString: null, changedAfterSearchDone: false }, searchPreviewList: createEmptyStateResource() }; const vorgangReducer = createReducer( initialState, - on(VorgangActions.loadVorgangList, (state, { }) => ({ + on(VorgangActions.loadVorgangList, (state: VorgangState): VorgangState => ({ ...state, vorgangList: { ...state.vorgangList, loading: true } })), - on(VorgangActions.loadVorgangListSuccess, (state, action) => { - return { - ...state, - vorgangList: createStateResource<VorgangListResource>(action.loadedResource), - vorgaenge: [...state.vorgaenge].concat(getVorgaengeFromList(action.loadedResource)), - searchPreviewList: createEmptyStateResource() - } - }), - on(VorgangActions.loadVorgangListFailure, (state, { apiError }) => ({ + on(VorgangActions.loadMyVorgaengeList, (state: VorgangState) => ({ ...state, - vorgangList: createErrorStateResource(apiError), + vorgangList: { ...state.vorgangList, loading: true } + })), + on(VorgangActions.loadVorgangListSuccess, (state: VorgangState, action: VorgangListAction) => ({ + ...state, + vorgangList: createStateResource<VorgangListResource>(action.vorgangList), + vorgaenge: getVorgaengeFromList(action.vorgangList), + searchPreviewList: createEmptyStateResource<VorgangListResource>() + })), + on(VorgangActions.loadVorgangListFailure, (state: VorgangState, action: ApiErrorAction): VorgangState => ({ + ...state, + vorgangList: createErrorStateResource(action.apiError), searchPreviewList: createEmptyStateResource() })), - on(VorgangActions.reloadVorgangList, (state, { searchString }) => ({ + + on(VorgangActions.loadNextPage, (state: VorgangState): VorgangState => ({ ...state, vorgangList: { ...state.vorgangList, reload: true }, - vorgaenge: EMPTY_ARRAY, - searchInfo: { ...state.searchInfo, searchString } + })), + on(VorgangActions.loadNextPageSuccess, (state, action: VorgangListAction): VorgangState => ({ + ...state, + vorgangList: createStateResource<VorgangListResource>(action.vorgangList), + vorgaenge: [...state.vorgaenge].concat(getVorgaengeFromList(action.vorgangList)), })), - on(VorgangActions.setSearchString, (state, { searchString }) => ({ + on(VorgangActions.searchVorgaengeBy, (state: VorgangState): VorgangState => ({ ...state, - searchInfo: { searchString, changedAfterSearchDone: true } + vorgangList: { ...state.vorgangList, loading: true }, + vorgaenge: EMPTY_ARRAY, + searchPreviewList: createEmptyStateResource() + })), + on(VorgangActions.searchVorgaengeBySuccess, (state: VorgangState, action: VorgangListAction): VorgangState => ({ + ...state, + vorgangList: createStateResource<VorgangListResource>(action.vorgangList), + vorgaenge: getVorgaengeFromList(action.vorgangList), + searchPreviewList: createEmptyStateResource<VorgangListResource>() })), - on(VorgangActions.searchForPreview, (state, { }) => ({ + + on(VorgangActions.searchForPreview, (state: VorgangState): VorgangState => ({ ...state, searchPreviewList: { ...state.searchPreviewList, loading: true } })), - on(VorgangActions.searchForPreviewSuccess, (state, { loadedResource }) => ({ + on(VorgangActions.searchForPreviewSuccess, (state: VorgangState, action: VorgangListAction): VorgangState => ({ ...state, - searchPreviewList: createStateResource(loadedResource) + searchPreviewList: createStateResource<VorgangListResource>(action.vorgangList) })), - on(VorgangActions.searchForPreviewFailure, (state, { apiError }) => ({ + on(VorgangActions.searchForPreviewFailure, (state: VorgangState, action: ApiErrorAction): VorgangState => ({ ...state, - searchPreviewList: createErrorStateResource(apiError) + searchPreviewList: createErrorStateResource(action.apiError) })), - on(VorgangActions.clearSearchPreviewList, (state, { }) => ({ + on(VorgangActions.clearSearchPreviewList, (state: VorgangState): VorgangState => ({ ...state, - searchPreviewList: createEmptyStateResource() + vorgangList: { ...state.vorgangList, reload: true }, + vorgaenge: EMPTY_ARRAY, + searchPreviewList: createEmptyStateResource<VorgangListResource>() })), + + + on(NavigationActions.updateCurrentRouteData, (state, action): VorgangState => { + return buildStateOnNavigation(state, action.routeData); + }) ); +function buildStateOnNavigation(state: VorgangState, routeData: RouteData): VorgangState { + const searchString: string = isSearch(routeData) ? getSearchString(routeData) : null; + + if (isMyVorgaenge(routeData)) { + return { + ...state, + vorgangList: { ...state.vorgangList, reload: state.vorgangList.loaded }, + searchInfo: { searchString, changedAfterSearchDone: true } + }; + } + if (isVorgangListPage(routeData)) { + return { + ...state, + searchInfo: { searchString, changedAfterSearchDone: false } + }; + } + if (isSearch(routeData)) { + const newState: VorgangState = { + ...state, + searchInfo: { ...state.searchInfo, searchString, changedAfterSearchDone: true }, + vorgangList: { ...state.vorgangList, reload: true }, + vorgaenge: EMPTY_ARRAY + } + if (hasSearchStringChanged(state, searchString)) { + return { ...newState, searchPreviewList: createEmptyStateResource() } + } + return newState; + } + if (isVorgangDetailPage(routeData)) { + return { + ...state, + searchInfo: { ...state.searchInfo, searchString: null, changedAfterSearchDone: false }, + vorgangList: { ...state.vorgangList, reload: true, resource: null }, + vorgaenge: EMPTY_ARRAY + }; + } + return { ...state }; +} + +function hasSearchStringChanged(state: VorgangState, currentSearchString: string): boolean { + return currentSearchString != state.searchInfo.searchString; +} + export function reducer(state: VorgangState, action: Action) { return vorgangReducer(state, action); } \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/vorgang-list.service.spec.ts b/goofy-client/libs/vorgang-shared/src/lib/vorgang-list.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..db668cc5f425be192bb5d0138f5e7a86d4cc72e5 --- /dev/null +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-list.service.spec.ts @@ -0,0 +1,281 @@ +import { ApiRootFacade, ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-shared'; +import { NavigationFacade, RouteData } from '@goofy-client/navigation-shared'; +import { createEmptyStateResource, createStateResource, StateResource } from '@goofy-client/tech-shared'; +import { Mock, mock, useFromMock } from '@goofy-client/test-utils'; +import { cold, hot } from 'jest-marbles'; +import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; +import { createRouteData } from 'libs/navigation-shared/test/navigation-test-factory'; +import { createVorgangListResource } from 'libs/vorgang-shared/test/vorgang'; +import { Observable } from 'rxjs'; +import { VorgangEffects } from './+state/vorgang.effects'; +import { VorgangFacade } from './+state/vorgang.facade'; +import { VorgangListService } from './vorgang-list.service'; +import { SearchInfo, VorgangListResource } from './vorgang.model'; + +describe('VorgangListService', () => { + let service: VorgangListService; + let vorgangFacade: Mock<VorgangFacade>; + let apiRootFacade: Mock<ApiRootFacade>; + let navigationFacade: Mock<NavigationFacade>; + + beforeEach(() => { + vorgangFacade = mock(VorgangFacade); + apiRootFacade = mock(ApiRootFacade); + navigationFacade = mock(NavigationFacade); + + service = new VorgangListService(useFromMock(vorgangFacade), useFromMock(apiRootFacade), useFromMock(navigationFacade)); + }) + + describe('getVorgangList', () => { + + describe('load required data', () => { + + it('should get vorgang list by facade', () => { + service.getVorgangList(); + + expect(vorgangFacade.getVorgangList).toHaveBeenCalled(); + }) + + it('should get apiroot by service', () => { + service.getVorgangList(); + + expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); + }) + + it('should get current route data by navigation facade', () => { + service.getVorgangList(); + + expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); + }) + }) + + describe('on loaded resource', () => { + + const vorgangListStateResource: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + const apiRootStateResource: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); + const currentRouteData: StateResource<RouteData> = createStateResource(createRouteData()); + + beforeEach(() => { + service.loadVorgangList = jest.fn(); + + vorgangFacade.getVorgangList.mockReturnValue(hot('-a', { a: vorgangListStateResource })); + apiRootFacade.getApiRoot.mockReturnValue(hot('-a', { a: apiRootStateResource })); + navigationFacade.getCurrentRouteData.mockReturnValue(hot('-a', { a: currentRouteData })); + }) + + it('should return loaded resource', () => { + const vorgangList = service.getVorgangList(); + + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: vorgangListStateResource })); + }) + + it('should not load vorgangList', () => { + service.getVorgangList(); + + expect(service.loadVorgangList).not.toHaveBeenCalled(); + }) + }) + + describe('on reload required', () => { + + const vorgangListResource: VorgangListResource = createVorgangListResource(); + const vorgangListStateResource: StateResource<VorgangListResource> = { ...createStateResource(vorgangListResource), reload: true }; + const apiRootStateResource: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); + const currentRouteData: StateResource<RouteData> = createStateResource(createRouteData()); + + beforeEach(() => { + service.loadVorgangList = jest.fn(); + + vorgangFacade.getVorgangList.mockReturnValue(hot('-a', { a: vorgangListStateResource })); + apiRootFacade.getApiRoot.mockReturnValue(hot('-a', { a: apiRootStateResource })); + navigationFacade.getCurrentRouteData.mockReturnValue(hot('-a', { a: currentRouteData })); + }) + + it('should return value on loaded resource', () => { + const vorgangList = service.getVorgangList(); + + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: { ...vorgangListStateResource, reload: true } })); + }) + + it.skip('FIXME: should load vorgangList', () => { + vorgangFacade.getVorgangList.mockReturnValue(hot('-ab', { a: { ...vorgangListStateResource, loading: true, reload: false }, b: { ...vorgangListStateResource, loading: true, reload: false } })); + + service.getVorgangList(); + + expect(service.loadVorgangList).toHaveBeenCalled(); + }) + }) + }) + + describe('loadVorgangList', () => { + + const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.SEARCH]); + const routeData: RouteData = createRouteData(); + + describe('on existing search string', () => { + + const searchString: string = 'search like me'; + + it('should call facade searchVorgaengeBy', () => { + service.loadVorgangList(apiRootResource, { ...routeData, queryParameter: { ['search']: searchString }, urlSegments: [<any>{ path: VorgangEffects.SEARCH_QUERY_PARAM }, { path: searchString }] }); + + expect(vorgangFacade.searchVorgaengeBy).toHaveBeenCalledWith(apiRootResource, searchString, ApiRootLinkRel.SEARCH); + }) + }) + + describe('on "myVorgaenge"', () => { + + it('should call facade loadMyVorgaengeList', () => { + service.loadVorgangList(apiRootResource, { ...routeData, urlSegments: [<any>{ path: 'myVorgaenge' }] }); + + expect(vorgangFacade.loadMyVorgaengeList).toHaveBeenCalledWith(apiRootResource); + }) + }) + + describe('on vorganglist loadVorgangList', () => { + + it('should call facade loadMyVorgaengeList', () => { + service.loadVorgangList(apiRootResource, routeData); + + expect(vorgangFacade.loadVorgangList).toHaveBeenCalledWith(apiRootResource); + }) + }) + }) + + describe('clearSearchPreviewList', () => { + + it('should call facade', () => { + service.clearSearchPreviewList(); + + expect(vorgangFacade.clearSearchPreviewList).toHaveBeenCalled(); + }) + }) + + describe('getVorgaenge', () => { + + it('should call facade', () => { + service.getVorgaenge(); + + expect(vorgangFacade.getVorgaenge).toHaveBeenCalled(); + }) + }) + + describe('loadNextPage', () => { + + it('should call facade', () => { + service.loadNextPage(); + + expect(vorgangFacade.loadNextPage).toHaveBeenCalled(); + }) + }) + + describe('getSearchInfo', () => { + + const searchInfo: SearchInfo = <any>{}; + + beforeEach(() => { + vorgangFacade.getSearchInfo.mockReturnValue(hot('a', { a: searchInfo })); + }) + + it('should call facade', () => { + service.getSearchInfo(); + + expect(vorgangFacade.getSearchInfo).toHaveBeenCalled(); + }) + + it.skip('FIXME: should return value', () => { + const searchInfo: Observable<SearchInfo> = service.getSearchInfo(); + + expect(searchInfo).toBeObservable(cold('a', { a: searchInfo })); + }) + }) + + describe('searchForPreview', () => { + + it('should call facade', () => { + const searchString: string = 'search like me'; + service.searchForPreview(searchString); + + expect(vorgangFacade.searchForPreview).toHaveBeenCalledWith(searchString); + }) + }) + + describe('getSearchPreviewList', () => { + + describe('load required data', () => { + + it('should get apiroot by service', () => { + service.getSearchPreviewList(); + + expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); + }) + + it('should get search preview list by facade', () => { + service.getSearchPreviewList(); + + expect(vorgangFacade.getSearchPreviewList).toHaveBeenCalled(); + }) + + it('should get search info by facade', () => { + service.getSearchPreviewList(); + + expect(vorgangFacade.getSearchInfo).toHaveBeenCalled(); + }) + }) + + describe('on loaded resource', () => { + + const vorgangListStateResource: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + const apiRootStateResource: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); + const searchString: string = 'search like me'; + const searchInfo: SearchInfo = <any>{ searchString }; + + beforeEach(() => { + service.searchForPreview = jest.fn(); + + vorgangFacade.getSearchPreviewList.mockReturnValue(hot('-a', { a: vorgangListStateResource })); + vorgangFacade.getSearchInfo.mockReturnValue(hot('-a', { a: searchInfo })); + apiRootFacade.getApiRoot.mockReturnValue(hot('-a', { a: apiRootStateResource })); + }) + + it('should return loaded resource', () => { + const vorgangList = service.getSearchPreviewList(); + + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: vorgangListStateResource })); + }) + + it('should not load vorgangList', () => { + service.getSearchPreviewList(); + + expect(service.searchForPreview).not.toHaveBeenCalled(); + }) + }) + + describe('on reload required', () => { + + const searchPreviewList: StateResource<VorgangListResource> = { ...createStateResource(createVorgangListResource()), reload: true }; + const apiRootStateResource: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); + const searchString: string = 'search like me'; + const searchInfo: SearchInfo = <any>{ searchString }; + + beforeEach(() => { + vorgangFacade.getSearchPreviewList.mockReturnValue(hot('-a', { a: searchPreviewList })); + vorgangFacade.getSearchInfo.mockReturnValue(hot('-a', { a: searchInfo })); + apiRootFacade.getApiRoot.mockReturnValue(hot('-a', { a: apiRootStateResource })); + }) + + it('should return value on loaded resource', () => { + const vorgangList = service.getSearchPreviewList(); + + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: { ...searchPreviewList, reload: true } })); + }) + + it.skip('FIXME: should call facade searchForPreview', () => { + const vorgangList = service.getSearchPreviewList(); + + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: { ...searchPreviewList, reload: true } })); + expect(vorgangFacade.searchForPreview).toHaveBeenCalledWith(searchPreviewList, searchString); + }) + }) + }) +}) \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/vorgang-list.service.ts b/goofy-client/libs/vorgang-shared/src/lib/vorgang-list.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..53077de656eedaf4d86bcc1634decf763a567255 --- /dev/null +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-list.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@angular/core'; +import { ApiRootFacade, ApiRootResource } from '@goofy-client/api-root-shared'; +import { NavigationFacade, RouteData } from '@goofy-client/navigation-shared'; +import { createEmptyStateResource, doIfLoadingRequired, EMPTY_STRING, isNotNull, StateResource } from '@goofy-client/tech-shared'; +import { combineLatest, Observable } from 'rxjs'; +import { map, startWith, tap } from 'rxjs/operators'; +import { VorgangFacade } from './+state/vorgang.facade'; +import { getSearchLinkRel, getSearchString, isMyVorgaenge, isSearch, isVorgangListPage } from './vorgang-navigation.util'; +import { SearchInfo, VorgangListResource, VorgangResource } from './vorgang.model'; + +@Injectable({ providedIn: 'root' }) +export class VorgangListService { + + constructor(private vorgangFacade: VorgangFacade, private apiRootFacade: ApiRootFacade, private navigationFacade: NavigationFacade) { } + + public getVorgangList(): Observable<StateResource<VorgangListResource>> { + return combineLatest([this.vorgangFacade.getVorgangList(), this.apiRootFacade.getApiRoot(), this.navigationFacade.getCurrentRouteData()]).pipe( + tap(([vorgangList, apiRoot, currentRouteData]) => { + if (isNotNull(apiRoot.resource)) { + doIfLoadingRequired(vorgangList, () => this.loadVorgangList(apiRoot.resource, currentRouteData)); + } + }), + map(([vorgangList, ,]) => vorgangList), + startWith(createEmptyStateResource<VorgangListResource>(true))); + } + + loadVorgangList(apiRoot: ApiRootResource, currentRouteData: RouteData): void { + if (isMyVorgaenge(currentRouteData)) { + this.vorgangFacade.loadMyVorgaengeList(apiRoot); + } + if (isVorgangListPage(currentRouteData)) { + this.vorgangFacade.loadVorgangList(apiRoot); + } + if (isSearch(currentRouteData)) { + this.vorgangFacade.searchVorgaengeBy(apiRoot, getSearchString(currentRouteData), getSearchLinkRel(currentRouteData)); + } + } + + public clearSearchPreviewList(): void { + this.vorgangFacade.clearSearchPreviewList(); + } + + public getVorgaenge(): Observable<VorgangResource[]> { + return this.vorgangFacade.getVorgaenge(); + } + + public loadNextPage(): void { + this.vorgangFacade.loadNextPage(); + } + + public getSearchInfo(): Observable<SearchInfo> { + return this.vorgangFacade.getSearchInfo(); + } + + public searchForPreview(searchString: string): void { + this.vorgangFacade.searchForPreview(searchString); + } + + public getSearchPreviewList(): Observable<StateResource<VorgangListResource>> { + return combineLatest([this.apiRootFacade.getApiRoot(), this.vorgangFacade.getSearchPreviewList(), this.vorgangFacade.getSearchInfo()]).pipe( + tap(([apiRoot, previewList, searchInfo]) => { + if (isNotNull(apiRoot.resource) && this.shouldSearchForPreview(previewList, searchInfo.searchString)) { + this.vorgangFacade.searchForPreview(searchInfo.searchString); + } + }), + map(([, previewList,]) => previewList), + startWith(createEmptyStateResource<VorgangListResource>(true))); + } + + shouldSearchForPreview(previewList: StateResource<VorgangListResource>, searchString: string): boolean { + return previewList.reload && !previewList.loading && (searchString != EMPTY_STRING); + } +} \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.spec.ts b/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.spec.ts index b11f78a897e58203d3f8e11874ec27946326cab7..64e15fbad865dd3b7f398ebe5cff0ce869428551 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.spec.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.spec.ts @@ -1,25 +1,25 @@ import { UrlSegment } from '@angular/router'; import { ApiRootLinkRel } from '@goofy-client/api-root-shared'; import { RouteData } from '@goofy-client/navigation-shared'; -import { createCurrentRouteData } from 'libs/navigation-shared/test/navigation-test-factory'; +import { createRouteData } from 'libs/navigation-shared/test/navigation-test-factory'; import { VorgangEffects } from './+state/vorgang.effects'; -import { existSearchString, getSearchLinkRel, getSearchString, isMyVorgaenge, navigateFromVorgangList } from './vorgang-navigation.util'; +import { getSearchLinkRel, getSearchString, isMyVorgaenge, isSearch, isVorgangDetailPage, isVorgangListPage } from './vorgang-navigation.util'; describe('Vorgang Navigation Util', () => { - describe('existSearchString', () => { + describe('isSearch', () => { - it('should return true is exists', () => { + it('should return true if search parameter exists', () => { const queryParameter = { [VorgangEffects.SEARCH_QUERY_PARAM]: 'searchThisForMe' }; - const currentRouteData: RouteData = { ...createCurrentRouteData(), queryParameter }; + const currentRouteData: RouteData = { ...createRouteData(), queryParameter }; - const exists: boolean = existSearchString(currentRouteData); + const exists: boolean = isSearch(currentRouteData); expect(exists).toBeTruthy(); }) it('should return false if NOT exists', () => { - const exists: boolean = existSearchString(createCurrentRouteData()); + const exists: boolean = isSearch(createRouteData()); expect(exists).toBeFalsy(); }) @@ -46,7 +46,7 @@ describe('Vorgang Navigation Util', () => { function buildCurrentRouteData(searchString: string): RouteData { const urlSegments: UrlSegment[] = [<any>{ path: VorgangEffects.SEARCH_QUERY_PARAM }, <any>{ path: searchString }]; const queryParameter = { [VorgangEffects.SEARCH_QUERY_PARAM]: searchString }; - return { ...createCurrentRouteData(), urlSegments, queryParameter }; + return { ...createRouteData(), urlSegments, queryParameter }; } }) @@ -54,7 +54,7 @@ describe('Vorgang Navigation Util', () => { it('should return "searchMyVorgaenge" linkrel', () => { const urlSegments: UrlSegment[] = [<any>{ path: VorgangEffects.MY_VORGAENGE_URI_SEGMENT }]; - const currentRouteData: RouteData = { ...createCurrentRouteData(), urlSegments }; + const currentRouteData: RouteData = { ...createRouteData(), urlSegments }; const linkRel: string = getSearchLinkRel(currentRouteData); @@ -62,7 +62,7 @@ describe('Vorgang Navigation Util', () => { }) it('should return "search" linkrel', () => { - const linkRel: string = getSearchLinkRel(createCurrentRouteData()); + const linkRel: string = getSearchLinkRel(createRouteData()); expect(linkRel).toBe(ApiRootLinkRel.SEARCH); }) @@ -72,7 +72,7 @@ describe('Vorgang Navigation Util', () => { it('should return true if "myVorgaenge" exists in urlSegments', () => { const urlSegments: UrlSegment[] = [<any>{ path: VorgangEffects.MY_VORGAENGE_URI_SEGMENT }]; - const currentRouteData: RouteData = { ...createCurrentRouteData(), urlSegments }; + const currentRouteData: RouteData = { ...createRouteData(), urlSegments }; const result: boolean = isMyVorgaenge(currentRouteData); @@ -80,14 +80,14 @@ describe('Vorgang Navigation Util', () => { }) it('should return false if urlSegments is empty', () => { - const result: boolean = isMyVorgaenge(createCurrentRouteData()); + const result: boolean = isMyVorgaenge(createRouteData()); expect(result).toBeFalsy(); }) it('should return false if urlSegments contains other data', () => { const urlSegments: UrlSegment[] = [<any>{ path: VorgangEffects.SEARCH_QUERY_PARAM }]; - const currentRouteData: RouteData = { ...createCurrentRouteData(), urlSegments }; + const currentRouteData: RouteData = { ...createRouteData(), urlSegments }; const result: boolean = isMyVorgaenge(currentRouteData); @@ -95,18 +95,39 @@ describe('Vorgang Navigation Util', () => { }) }) - describe('navigateFromVorgangList', () => { + describe('isVorgangListPage', () => { - it('should return true is queryParamter is empty', () => { - const result: boolean = navigateFromVorgangList(createCurrentRouteData()); + it('should return true if queryParameter is empty', () => { + const result: boolean = isVorgangListPage(createRouteData()); - expect(result).toBeTruthy() + expect(result).toBeTruthy(); + }) + + it('should return false if queryParameter is not empty', () => { + const result: boolean = isVorgangListPage({ ...createRouteData(), queryParameter: { ['search']: 'exampleParameter' } }); + + expect(result).toBeFalsy(); }) - it('should return false if queryParameter is NOT empty', () => { - const currentRouteData: RouteData = { ...createCurrentRouteData(), queryParameter: { ['']: 'myVorgaenge' } }; + it('should return false if urlSegments contains for exmaple "myVorgaenge"', () => { + const urlSegments: UrlSegment[] = [<any>{ path: VorgangEffects.MY_VORGAENGE_URI_SEGMENT }]; + const currentRouteData: RouteData = { ...createRouteData(), urlSegments }; + const result: boolean = isVorgangListPage(currentRouteData); + + expect(result).toBeFalsy(); + }) + }) + + describe('isVorgangDetailPage', () => { + + it('should return true if queryParameter contains "vorgangWithEingangUrl"', () => { + const result: boolean = isVorgangDetailPage({ ...createRouteData(), queryParameter: { ['vorgangWithEingangUrl']: 'encodedUri' } }); + + expect(result).toBeTruthy(); + }) - const result: boolean = navigateFromVorgangList(currentRouteData); + it('should return false if queryParameter NOT contains "vorgangWithEingangUrl"', () => { + const result: boolean = isVorgangDetailPage({ ...createRouteData(), queryParameter: { ['search']: 'exampleParameter' } }); expect(result).toBeFalsy(); }) diff --git a/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.ts b/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.ts index 43d48c537b094241df561e515961e08214b713e0..181b62b0b15b7539889258416a99bf8fe3a9e76b 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.ts @@ -3,27 +3,39 @@ import { RouteData } from '@goofy-client/navigation-shared'; import { isEmptyObject, isNotUndefined } from '@goofy-client/tech-shared'; import { VorgangEffects } from './+state/vorgang.effects'; -export function existSearchString(routeData: RouteData): boolean { +export function isSearch(routeData: RouteData): boolean { return isNotUndefined(routeData.queryParameter[VorgangEffects.SEARCH_QUERY_PARAM]); } -export function getSearchString(currentRouteData: RouteData): string { - const searchString: string = currentRouteData.urlSegments[getSearchStringIndex(currentRouteData) + 1].path.toString(); +export function getSearchString(routeData: RouteData): string { + const searchString: string = routeData.urlSegments[getSearchStringIndex(routeData) + 1].path.toString(); return decodeURIComponent(searchString); } -function getSearchStringIndex(currentRouteData: RouteData): number { - return currentRouteData.urlSegments.findIndex(segment => segment.path.toString() == VorgangEffects.SEARCH_QUERY_PARAM); +function getSearchStringIndex(routeData: RouteData): number { + return routeData.urlSegments.findIndex(segment => segment.path.toString() == VorgangEffects.SEARCH_QUERY_PARAM); } -export function getSearchLinkRel(currentRouteData: RouteData): string { - return isMyVorgaenge(currentRouteData) ? ApiRootLinkRel.SEARCH_MY_VORGAENGE : ApiRootLinkRel.SEARCH; +export function getSearchLinkRel(routeData: RouteData): string { + return containsMyVorgaenge(routeData) ? ApiRootLinkRel.SEARCH_MY_VORGAENGE : ApiRootLinkRel.SEARCH; } -export function isMyVorgaenge(currentRouteData: RouteData): boolean { - return currentRouteData.urlSegments.length > 0 && currentRouteData.urlSegments[0].path.toString() == VorgangEffects.MY_VORGAENGE_URI_SEGMENT; +export function isMyVorgaenge(routeData: RouteData): boolean { + return getPrimarySegmentPath(routeData) == VorgangEffects.MY_VORGAENGE_URI_SEGMENT && !isSearch(routeData); } -export function navigateFromVorgangList(routeData: RouteData): boolean { - return isEmptyObject(routeData.queryParameter) ? true : false; +export function containsMyVorgaenge(routeData: RouteData): boolean { + return getPrimarySegmentPath(routeData) == VorgangEffects.MY_VORGAENGE_URI_SEGMENT; +} + +function getPrimarySegmentPath(routeData: RouteData): string { + return routeData.urlSegments.length > 0 && routeData.urlSegments[0].path.toString(); +} + +export function isVorgangListPage(routeData: RouteData): boolean { + return isEmptyObject(routeData.queryParameter) && !isMyVorgaenge(routeData) && !isSearch(routeData); +} + +export function isVorgangDetailPage(routeData: RouteData): boolean { + return isNotUndefined(routeData.queryParameter['vorgangWithEingangUrl']); } \ No newline at end of file diff --git a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.spec.ts b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.spec.ts index ac823211d4f8f316143ca03ed35f085b9dd94bc5..6681f689f21647c427a337f94a45f28428c5b2cc 100644 --- a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.spec.ts +++ b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { EMPTY_STRING } from '@goofy-client/tech-shared'; import { mock } from '@goofy-client/test-utils'; -import { SearchInfo, VorgangFacade } from '@goofy-client/vorgang-shared'; +import { SearchInfo, VorgangListService } from '@goofy-client/vorgang-shared'; import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { VorgangListContainerComponent } from './vorgang-list-container.component'; @@ -12,7 +12,7 @@ describe('VorgangListContainerComponent', () => { let fixture: ComponentFixture<VorgangListContainerComponent>; const searchInfo: SearchInfo = { searchString: EMPTY_STRING, changedAfterSearchDone: false }; - const vorgangFacade = { ...mock(VorgangFacade), getSearchInfo: jest.fn().mockReturnValue(of(searchInfo)) }; + const vorgangListService = { ...mock(VorgangListService), getSearchInfo: jest.fn().mockReturnValue(of(searchInfo)) }; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -22,8 +22,8 @@ describe('VorgangListContainerComponent', () => { ], providers: [ { - provide: VorgangFacade, - useValue: vorgangFacade + provide: VorgangListService, + useValue: vorgangListService } ] }) @@ -44,19 +44,19 @@ describe('VorgangListContainerComponent', () => { it('should call facade to get vorgaenge', () => { component.ngOnInit(); - expect(vorgangFacade.getVorgaenge).toHaveBeenCalled(); + expect(vorgangListService.getVorgaenge).toHaveBeenCalled(); }) it('should call facade to get vorgangList', () => { component.ngOnInit(); - expect(vorgangFacade.getVorgangList).toHaveBeenCalled(); + expect(vorgangListService.getVorgangList).toHaveBeenCalled(); }) it('should call facade to get searchInfo', () => { component.ngOnInit(); - expect(vorgangFacade.getSearchInfo).toHaveBeenCalled(); + expect(vorgangListService.getSearchInfo).toHaveBeenCalled(); }) }) @@ -65,7 +65,7 @@ describe('VorgangListContainerComponent', () => { it('should call facade loadNextPage', () => { component.loadNextPage(); - expect(vorgangFacade.loadNextPage).toHaveBeenCalled(); + expect(vorgangListService.loadNextPage).toHaveBeenCalled(); }) }) }); diff --git a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.ts b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.ts index 7c92fd6f34d0160f3d3685dab4d663e74b6bda5c..f89d44a0dc30b1d7f91f4749c39432aa029dc04e 100644 --- a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.ts +++ b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { StateResource } from '@goofy-client/tech-shared'; -import { VorgangFacade, VorgangListResource, VorgangResource } from '@goofy-client/vorgang-shared'; +import { VorgangListResource, VorgangListService, VorgangResource } from '@goofy-client/vorgang-shared'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -15,15 +15,15 @@ export class VorgangListContainerComponent implements OnInit { public vorgaenge$: Observable<VorgangResource[]> public searchString$: Observable<string>; - constructor(private vorgangFacade: VorgangFacade) { } + constructor(private vorgangListService: VorgangListService) { } ngOnInit(): void { - this.vorgaenge$ = this.vorgangFacade.getVorgaenge(); - this.vorgangListPageResource$ = this.vorgangFacade.getVorgangList(); - this.searchString$ = this.vorgangFacade.getSearchInfo().pipe(map(searchInfo => searchInfo.searchString)); + this.vorgaenge$ = this.vorgangListService.getVorgaenge(); + this.vorgangListPageResource$ = this.vorgangListService.getVorgangList(); + this.searchString$ = this.vorgangListService.getSearchInfo().pipe(map(searchInfo => searchInfo.searchString)); } loadNextPage(): void { - this.vorgangFacade.loadNextPage(); + this.vorgangListService.loadNextPage(); } }