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/components/vorgang/vorgang.formular-daten.historie.e2e.component.ts b/goofy-client/apps/goofy-e2e/src/components/vorgang/vorgang.formular-daten.historie.e2e.component.ts index 905a399029ebf4539c876e773e33773753c9c20b..ea96a05f12dfbb8748d38c70a0519a33c487ca63 100644 --- a/goofy-client/apps/goofy-e2e/src/components/vorgang/vorgang.formular-daten.historie.e2e.component.ts +++ b/goofy-client/apps/goofy-e2e/src/components/vorgang/vorgang.formular-daten.historie.e2e.component.ts @@ -5,15 +5,15 @@ export class VorgangFormularDatenHistorieE2EComponent { private readonly locatorRoot: string = 'historie-in-vorgang-container'; private readonly locatorCreatedAt: string = 'historie-vorgang-created-at'; - getRoot() { + public getRoot() { return getTestElement(this.locatorRoot); } - getCreatedAt() { + public getCreatedAt() { return getTestElement(this.locatorCreatedAt); } - getListItemByIndex(index: number): VorgangFormularDatenHistorieItemE2EComponent { + public getListItemByIndex(index: number): VorgangFormularDatenHistorieItemE2EComponent { return new VorgangFormularDatenHistorieItemE2EComponent(index); } } @@ -22,43 +22,50 @@ export class VorgangFormularDatenHistorieItemE2EComponent { private readonly locatorRoot: string = 'historie-list-item-'; private readonly locatorHeadline: string = 'historie-item-header'; + private readonly locatorUser: string = 'user-profile-in-historie-item-header'; + private readonly locatorSystemUser: string = 'system-user-in-historie-item-header'; private readonly locatorExpandButton: string = 'expansion-button'; constructor(private itemIndex: number) { this.locatorRoot = 'historie-list-item-' + this.itemIndex; } - getRoot() { + public getRoot() { return getTestElement(this.locatorRoot); } - getHeadline() { - return this.getRoot().getTestElementWithClass(this.locatorHeadline); + public getHeadline() { + return this.getRoot().findTestElementWithClass(this.locatorHeadline); } - //CLEANME: Workaround, eigtl sollte das per getRoot().getTestElementWithClass() gefunden werden :/ - expand(): void { - let count = 0; - this.getExpandButton().each(($ele) => { - if (count == this.itemIndex) { - $ele.click(); - } - count++; - }) + public getUser() { + return this.getRoot().findTestElementWithClass(this.locatorUser); } - private getExpandButton() { - return this.getRoot().getTestElementWithClass(this.locatorExpandButton); + public getSystemUser() { + return this.getRoot().findTestElementWithClass(this.locatorSystemUser); } - getPostfachNachricht(): PostfachNachrichtHistorieItemE2EComponent { + public getExpandButton() { + return this.getRoot().findTestElementWithClass(this.locatorExpandButton); + } + + public getPostfachNachricht(): PostfachNachrichtHistorieItemE2EComponent { return new PostfachNachrichtHistorieItemE2EComponent(this.locatorRoot); } + + public getWiedervorlage(): WiedervorlageHistorieItemE2EComponent { + return new WiedervorlageHistorieItemE2EComponent(this.locatorRoot); + } + + public getKommentar(): KommentarHistorieItemE2EComponent { + return new KommentarHistorieItemE2EComponent(this.locatorRoot); + } } export class PostfachNachrichtHistorieItemE2EComponent { - private readonly locatorRoot: string = 'historie-list-item-'; + private readonly locatorRoot: string; private readonly locatorPostfachMailBody: string = 'postfach-nachricht-mail-body'; private readonly locatorPostfachSubject: string = 'postfach-nachricht-subject'; @@ -67,15 +74,68 @@ export class PostfachNachrichtHistorieItemE2EComponent { this.locatorRoot = rootLocator; } - getRoot() { + public getRoot() { return getTestElement(this.locatorRoot); } - getPostfachNachrichtSubject() { - return this.getRoot().getTestElementWithClass(this.locatorPostfachSubject); + public getPostfachNachrichtSubject() { + return this.getRoot().findTestElementWithClass(this.locatorPostfachSubject); + } + + public getPostfachNachrichtMailBody() { + return this.getRoot().findTestElementWithClass(this.locatorPostfachMailBody); + } +} + +export class WiedervorlageHistorieItemE2EComponent { + + private readonly locatorRoot: string; + + private readonly locatorStatus: string = 'wiedervorlage-status'; + private readonly locatorBetreff: string = 'wiedervorlage-betreff'; + private readonly locatorBeschreibung: string = 'wiedervorlage-beschreibung'; + private readonly locatorAttachment: string = 'historie-item-attachment'; + + constructor(rootLocator: string) { + this.locatorRoot = rootLocator; + } + + public getRoot() { + return getTestElement(this.locatorRoot); + } + + public getStatus() { + return this.getRoot().findTestElementWithClass(this.locatorStatus); + } + + public getBetreff() { + return this.getRoot().findTestElementWithClass(this.locatorBetreff); + } + + public getBeschreibung() { + return this.getRoot().findTestElementWithClass(this.locatorBeschreibung); + } + + public getAttachment() { + return this.getRoot().findTestElementWithClass(this.locatorAttachment); + } +} + +export class KommentarHistorieItemE2EComponent { + + private readonly locatorRoot: string; + + private readonly locatorText: string = 'kommentar-text'; + + constructor(rootLocator: string) { + this.locatorRoot = rootLocator; + } + + public getRoot() { + return getTestElement(this.locatorRoot); } - getPostfachNachrichtMailBody() { - return this.getRoot().getTestElementWithClass(this.locatorPostfachMailBody); + public getText() { + return this.getRoot().findTestElementWithClass(this.locatorText); } } \ No newline at end of file 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/historie/historie.e2e-spec.ts b/goofy-client/apps/goofy-e2e/src/integration/main-tests/historie/historie.e2e-spec.ts index 695778fb1e4550c0b3fb12a5169dcf30dec9a30d..7db97cd37d7125679d3383f2384f86ddfce5970b 100644 --- a/goofy-client/apps/goofy-e2e/src/integration/main-tests/historie/historie.e2e-spec.ts +++ b/goofy-client/apps/goofy-e2e/src/integration/main-tests/historie/historie.e2e-spec.ts @@ -1,12 +1,15 @@ import { registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; import localeDeExtra from '@angular/common/locales/extra/de'; -import { PostfachNachrichtHistorieItemE2EComponent, VorgangFormularDatenHistorieItemE2EComponent } from 'apps/goofy-e2e/src/components/vorgang/vorgang.formular-daten.historie.e2e.component'; +import { KommentarHistorieItemE2EComponent, PostfachNachrichtHistorieItemE2EComponent, VorgangFormularDatenHistorieItemE2EComponent, WiedervorlageHistorieItemE2EComponent } from 'apps/goofy-e2e/src/components/vorgang/vorgang.formular-daten.historie.e2e.component'; import { CommandE2E, CommandOrderE2E } from 'apps/goofy-e2e/src/model/command'; -import { HistorieHeadlineE2E } from 'apps/goofy-e2e/src/model/historie'; +import { HistorieAttachmentE2E, HistorieHeadlineE2E, SYSTEM_USER_NAME } from 'apps/goofy-e2e/src/model/historie'; import { PostfachE2E } from 'apps/goofy-e2e/src/model/postfach-nachricht'; +import { DirectionE2E } from 'apps/goofy-e2e/src/model/vorgang-attached-item'; import { buildCommand, createCommand, initCommands } from 'apps/goofy-e2e/src/support/command-util'; +import { createKommentar } from 'apps/goofy-e2e/src/support/kommentar.util'; import { createPostfachNachricht } from 'apps/goofy-e2e/src/support/postfach-util'; +import { createWiedervorlageItem } from 'apps/goofy-e2e/src/support/wiedervorlage-util'; import { VorgangDetailHeaderE2EComponent } from '../../../components/vorgang/vorgang-detail-header.e2e.component'; import { VorgangFormularDatenE2EComponent } from '../../../components/vorgang/vorgang-formular.e2e.component'; import { VorgangListE2EComponent } from '../../../components/vorgang/vorgang-list.e2e.component'; @@ -14,8 +17,8 @@ import { VorgangE2E } from '../../../model/vorgang'; import { MainPage, waitForSpinnerToDisappear } from '../../../page-objects/main.po'; import { VorgangPage } from '../../../page-objects/vorgang.po'; import { dropCollections } from '../../../support/cypress-helper'; -import { contains, exist } from '../../../support/cypress.util'; -import { loginAsSabine } from '../../../support/user-util'; +import { contains, exist, notExist } from '../../../support/cypress.util'; +import { getUserSabine, getUserSabineUuid, loginAsSabine } from '../../../support/user-util'; import { createVorgang, initVorgang } from '../../../support/vorgang-util'; registerLocaleData(localeDe, 'de', localeDeExtra); @@ -31,30 +34,108 @@ describe('Historie', () => { const vorgang: VorgangE2E = createVorgang(); + const assignUserCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.ASSIGN_USER, vorgang._id.$oid, vorgang._id.$oid), + bodyObject: { assignedTo: getUserSabineUuid() }, + createdAt: { $date: '2020-01-01T10:00:00.000Z' } + }; + + const kommentarCommandBody = { itemName: 'Kommentar', item: createKommentar() }; + const createKommentarCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.CREATE_ATTACHED_ITEM, vorgang._id.$oid, vorgang._id.$oid), + bodyObject: kommentarCommandBody, + createdAt: { $date: '2020-01-01T10:01:00.000Z' } + }; + const editKommentarCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.UPDATE_ATTACHED_ITEM, vorgang._id.$oid, vorgang._id.$oid), + bodyObject: kommentarCommandBody, + createdAt: { $date: '2020-01-01T10:02:00.000Z' } + }; + + const forwardVorgangCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.FORWARD_VORGANG, vorgang._id.$oid, vorgang._id.$oid), + createdAt: { $date: '2020-01-01T10:03:00.000Z' } + }; + const forwardSuccessfulCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.FORWARD_SUCCESSFUL, vorgang._id.$oid, vorgang._id.$oid), + createdAt: { $date: '2020-01-01T10:04:00.000Z' } + }; + const forwardFailedCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.FORWARD_FAILED, vorgang._id.$oid, vorgang._id.$oid), + createdAt: { $date: '2020-01-01T10:05:00.000Z' } + }; + const sendPostfachMailCommandBody: PostfachE2E = { ...createPostfachNachricht(), mailBody: 'With mail test body!', - subject: 'Send Postfach Mail' + subject: 'Send Postfach Mail', + attachments: 'dummyAttachment' }; - const sendPostfachMailCommand: CommandE2E = { ...createCommand(), - body: sendPostfachMailCommandBody + bodyObject: sendPostfachMailCommandBody, + createdAt: { $date: '2020-01-01T10:06:00.000Z' } }; - const sendPostfachNachrichtCommandBody: PostfachE2E = { ...createPostfachNachricht(), mailBody: 'With nachricht test body!', - subject: 'Send Postfach Nachricht' + subject: 'Send Postfach Nachricht', + attachments: ['dummyAttachment', 'dummyAttachment2'] }; const sendPostfachNachrichtCommand: CommandE2E = { ...buildCommand(CommandOrderE2E.SEND_POSTFACH_NACHRICHT, vorgang._id.$oid, vorgang._id.$oid), - body: sendPostfachNachrichtCommandBody + bodyObject: sendPostfachNachrichtCommandBody, + createdAt: { $date: '2020-01-01T10:07:00.000Z' } + }; + const receivePostfachNachrichtCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.CREATE_ATTACHED_ITEM, vorgang._id.$oid, vorgang._id.$oid), + bodyObject: { + itemName: 'PostfachMail', + item: { + mailBody: 'With nachricht test body!', + attachments: ['dummyAttachment', 'dummyAttachment2'], + subject: 'AW: Send Postfach Nachricht', + direction: DirectionE2E.IN + }, + client: 'MailService', + vorgangId: vorgang._id.$oid + }, + createdBy: null, + createdAt: { $date: '2020-01-01T10:08:00.000Z' } + }; + + const createWiedervorlageCommandBody = { ...createWiedervorlageItem('Create Wiedervorlage Betreff'), attachments: 'DummyAttachment' }; + const createWiedervorlageCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.CREATE_ATTACHED_ITEM, vorgang._id.$oid, vorgang._id.$oid), + bodyObject: { itemName: 'Wiedervorlage', item: createWiedervorlageCommandBody }, + createdAt: { $date: '2020-01-01T10:09:00.000Z' } + }; + const editWiedervorlageCommandBody = { ...createWiedervorlageItem('Edit Wiedervorlage Betreff'), attachments: ['DummyAttachment', 'DummyAttachment2'], done: true }; + const editWiedervorlageCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.UPDATE_ATTACHED_ITEM, vorgang._id.$oid, vorgang._id.$oid), + bodyObject: { itemName: 'Wiedervorlage', item: editWiedervorlageCommandBody }, + createdAt: { $date: '2020-01-01T10:10:00.000Z' } + }; + const wiedervorlageErledigenCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.PATCH_ATTACHED_ITEM, vorgang._id.$oid, vorgang._id.$oid), + bodyObject: { itemName: 'Wiedervorlage', item: { done: 'true' }, vorgangId: vorgang._id.$oid }, + createdAt: { $date: '2020-01-01T10:11:00.000Z' } + }; + const wiedervorlageWiedereroeffnenCommand: CommandE2E = { + ...buildCommand(CommandOrderE2E.PATCH_ATTACHED_ITEM, vorgang._id.$oid, vorgang._id.$oid), + bodyObject: { itemName: 'Wiedervorlage', item: { done: 'false' }, vorgangId: vorgang._id.$oid }, + createdAt: { $date: '2020-01-01T10:12:00.000Z' } }; before(() => { initVorgang(vorgang); - initCommands([sendPostfachMailCommand, sendPostfachNachrichtCommand]); + initCommands([ + assignUserCommand, + createKommentarCommand, editKommentarCommand, + forwardVorgangCommand, forwardSuccessfulCommand, forwardFailedCommand, + sendPostfachMailCommand, sendPostfachNachrichtCommand, receivePostfachNachrichtCommand, + createWiedervorlageCommand, editWiedervorlageCommand, wiedervorlageErledigenCommand, wiedervorlageWiedereroeffnenCommand + ]); loginAsSabine(); @@ -90,33 +171,178 @@ describe('Historie', () => { exist(vorgangDatenFormular.getHistorieContainer().getCreatedAt()); }) - describe('should show historie item for order', () => { + describe('should show historie item for', () => { + + const userName: string = `${getUserSabine().firstName} ${getUserSabine().lastName}`; + + describe('vorgang order', () => { + + it('assign user', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(0); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.ASSIGN_USER); + contains(historieItem.getUser(), userName); + notExist(historieItem.getExpandButton()); + }) + }) + + describe('kommentar order', () => { + + it('create kommentar', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(1); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.CREATE_KOMMENTAR); + contains(historieItem.getUser(), userName); + + historieItem.getExpandButton().click(); + const kommentarItem: KommentarHistorieItemE2EComponent = historieItem.getKommentar(); - it('send postfach mail', () => { - const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(0); + contains(kommentarItem.getText(), kommentarCommandBody.item.text); + }) - exist(historieItem.getRoot()); - contains(historieItem.getHeadline(), HistorieHeadlineE2E.SEND_POSTFACH_NACHRICHT); + it('edit kommentar', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(2); - historieItem.expand(); - const postfachNachtichtItem: PostfachNachrichtHistorieItemE2EComponent = historieItem.getPostfachNachricht(); + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.EDIT_KOMMENTAR); + contains(historieItem.getUser(), userName); + historieItem.getExpandButton().click(); + const kommentarItem: KommentarHistorieItemE2EComponent = historieItem.getKommentar(); - contains(postfachNachtichtItem.getPostfachNachrichtSubject(), sendPostfachMailCommandBody.subject); - contains(postfachNachtichtItem.getPostfachNachrichtMailBody(), sendPostfachMailCommandBody.mailBody); + contains(kommentarItem.getText(), kommentarCommandBody.item.text); + }) }) - it('send postfach nachricht', () => { - const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(1); + describe('forward order', () => { + + it('forward vorgang', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(3); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.FORWARD_VORGANG); + contains(historieItem.getUser(), userName); + notExist(historieItem.getExpandButton()); + }) + + it('foward successful', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(4); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.FORWARD_SUCCESSFUL); + contains(historieItem.getUser(), userName); + notExist(historieItem.getExpandButton()); + }) + + it('foward failed', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(5); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.FORWARD_FAILED); + contains(historieItem.getUser(), userName); + notExist(historieItem.getExpandButton()); + }) + }) + + describe('postfach nachricht order', () => { + + it('send postfach mail', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(6); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.SEND_POSTFACH_NACHRICHT); + contains(historieItem.getUser(), userName); + + historieItem.getExpandButton().click(); + const postfachNachtichtItem: PostfachNachrichtHistorieItemE2EComponent = historieItem.getPostfachNachricht(); + + contains(postfachNachtichtItem.getPostfachNachrichtSubject(), sendPostfachMailCommandBody.subject); + contains(postfachNachtichtItem.getPostfachNachrichtMailBody(), sendPostfachMailCommandBody.mailBody); + }) + + it('send postfach nachricht', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(7); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.SEND_POSTFACH_NACHRICHT); + contains(historieItem.getUser(), userName); + + historieItem.getExpandButton().click(); + const postfachNachtichtItem: PostfachNachrichtHistorieItemE2EComponent = historieItem.getPostfachNachricht(); + + contains(postfachNachtichtItem.getPostfachNachrichtSubject(), sendPostfachNachrichtCommandBody.subject); + contains(postfachNachtichtItem.getPostfachNachrichtMailBody(), sendPostfachNachrichtCommandBody.mailBody); + }) + + it('receive postfach nachricht', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(8); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.RECEIVE_POSTFACH_NACHRICHT); + notExist(historieItem.getUser()); + contains(historieItem.getSystemUser(), SYSTEM_USER_NAME); + + historieItem.getExpandButton().click(); + const postfachNachtichtItem: PostfachNachrichtHistorieItemE2EComponent = historieItem.getPostfachNachricht(); + + contains(postfachNachtichtItem.getPostfachNachrichtSubject(), receivePostfachNachrichtCommand.bodyObject.item.subject); + contains(postfachNachtichtItem.getPostfachNachrichtMailBody(), receivePostfachNachrichtCommand.bodyObject.item.mailBody); + }) + }) + + describe('wiedervorlage order', () => { + + it('create wiedervorlage', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(9); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.CREATE_WIEDERVORLAGE); + contains(historieItem.getUser(), userName); + + historieItem.getExpandButton().click(); + const wiedervorlageItem: WiedervorlageHistorieItemE2EComponent = historieItem.getWiedervorlage(); + + contains(wiedervorlageItem.getStatus(), 'offen'); + contains(wiedervorlageItem.getBetreff(), createWiedervorlageCommandBody.betreff); + contains(wiedervorlageItem.getBeschreibung(), createWiedervorlageCommandBody.beschreibung); + contains(wiedervorlageItem.getAttachment(), HistorieAttachmentE2E.SINGLE_TEXT); + }) + + it('edit wiedervorlage', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(10); + + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.EDIT_WIEDERVORLAGE); + contains(historieItem.getUser(), userName); + + historieItem.getExpandButton().click(); + const wiedervorlageItem: WiedervorlageHistorieItemE2EComponent = historieItem.getWiedervorlage(); + + contains(wiedervorlageItem.getStatus(), 'geschlossen'); + contains(wiedervorlageItem.getBetreff(), editWiedervorlageCommandBody.betreff); + contains(wiedervorlageItem.getBeschreibung(), editWiedervorlageCommandBody.beschreibung); + contains(wiedervorlageItem.getAttachment(), HistorieAttachmentE2E.MULTIPLE_TEXT.replace('{countAttachments}', '2')); + }) + + it('wiedervorlage erledigen', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(11); - exist(historieItem.getRoot()); - contains(historieItem.getHeadline(), HistorieHeadlineE2E.SEND_POSTFACH_NACHRICHT); + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.WIEDERVORLAGE_ERLEDIGEN); + contains(historieItem.getUser(), userName); + notExist(historieItem.getExpandButton()); + }) - historieItem.expand(); - const postfachNachtichtItem: PostfachNachrichtHistorieItemE2EComponent = historieItem.getPostfachNachricht(); + it('wiedervorlage wiedereroeffnen', () => { + const historieItem: VorgangFormularDatenHistorieItemE2EComponent = vorgangDatenFormular.getHistorieContainer().getListItemByIndex(12); - contains(postfachNachtichtItem.getPostfachNachrichtSubject(), sendPostfachNachrichtCommandBody.subject); - contains(postfachNachtichtItem.getPostfachNachrichtMailBody(), sendPostfachNachrichtCommandBody.mailBody); + exist(historieItem.getRoot()); + contains(historieItem.getHeadline(), HistorieHeadlineE2E.WIEDERVORLAGE_WIEDEREROEFFNEN); + contains(historieItem.getUser(), userName); + notExist(historieItem.getExpandButton()); + }) }) }) }) 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/apps/goofy-e2e/src/model/command.ts b/goofy-client/apps/goofy-e2e/src/model/command.ts index 6c02b092f2f38db17982536486e32deb4ecd40e2..f20f1e98c6a2c2fd39372b5567668b28e28df313 100644 --- a/goofy-client/apps/goofy-e2e/src/model/command.ts +++ b/goofy-client/apps/goofy-e2e/src/model/command.ts @@ -1,8 +1,16 @@ -import { DateE2E } from "./util"; +import { DateE2E } from './util'; export enum CommandOrderE2E { + ASSIGN_USER = 'ASSIGN_USER', + CREATE_ATTACHED_ITEM = 'CREATE_ATTACHED_ITEM', + FORWARD_VORGANG = 'REDIRECT_VORGANG',//TODO Rename Order + FORWARD_SUCCESSFUL = 'FORWARD_SUCCESSFULL', //TODO Rename Order + FORWARD_FAILED = 'FORWARD_FAILED', + PATCH_ATTACHED_ITEM = 'PATCH_ATTACHED_ITEM', SEND_POSTFACH_MAIL = 'SEND_POSTFACH_MAIL', - SEND_POSTFACH_NACHRICHT = 'SEND_POSTFACH_NACHRICHT' + SEND_POSTFACH_NACHRICHT = 'SEND_POSTFACH_NACHRICHT', + RECEIVE_POSTFACH_NACHRICHT = 'RECEIVE_POSTFACH_NACHRICHT', + UPDATE_ATTACHED_ITEM = 'UPDATE_ATTACHED_ITEM' } export class CommandE2E { vorgangId: string; @@ -13,7 +21,7 @@ export class CommandE2E { relationId: string; relationVersion: number; order: CommandOrderE2E; - body?: object; - bodyObject: object; + body?: any; + bodyObject: any; finishedAt: DateE2E; } \ No newline at end of file diff --git a/goofy-client/apps/goofy-e2e/src/model/historie.ts b/goofy-client/apps/goofy-e2e/src/model/historie.ts index dd40fcc055fef347c3ea80fa0f576f068f8bb06d..e77315772f903a2309811aad2ea6cd84fecff830 100644 --- a/goofy-client/apps/goofy-e2e/src/model/historie.ts +++ b/goofy-client/apps/goofy-e2e/src/model/historie.ts @@ -1,10 +1,28 @@ -import { PostfachE2E } from "./postfach-nachricht"; +import { PostfachE2E } from './postfach-nachricht'; export enum HistorieHeadlineE2E { - SEND_POSTFACH_NACHRICHT = 'eine Nachricht geschrieben' + ASSIGN_USER = 'den Vorgang zugewiesen.', + CREATE_WIEDERVORLAGE = 'eine Wiedervorlage hinzugefügt.', + CREATE_KOMMENTAR = 'ein Kommentar hinzugefügt', + EDIT_WIEDERVORLAGE = 'eine Wiedervorlage bearbeitet.', + EDIT_KOMMENTAR = 'ein Kommentar bearbeitet.', + FORWARD_VORGANG = ' den Vorgang weitergeleitet', + FORWARD_SUCCESSFUL = 'die Weiterleitung bestätigt.', + FORWARD_FAILED = 'die Weiterleitung widerrufen.', + SEND_POSTFACH_NACHRICHT = 'eine Nachricht geschrieben', + RECEIVE_POSTFACH_NACHRICHT = 'eine Nachricht des Antragstellers empfangen.', + WIEDERVORLAGE_ERLEDIGEN = 'eine Wiedervorlage als erledigt markiert.', + WIEDERVORLAGE_WIEDEREROEFFNEN = 'eine Wiedervorlage als offen markiert.', } export class HistorieE2E { headline: HistorieHeadlineE2E; body: PostfachE2E; -} \ No newline at end of file +} + +export enum HistorieAttachmentE2E { + SINGLE_TEXT = 'Es existiert ein Anhang zu diesem Eintrag.', + MULTIPLE_TEXT = 'Es existieren {countAttachments} Anhänge zu diesem Eintrag.' +} + +export const SYSTEM_USER_NAME = 'Die Anwendung'; \ No newline at end of file diff --git a/goofy-client/apps/goofy-e2e/src/model/postfach-nachricht.ts b/goofy-client/apps/goofy-e2e/src/model/postfach-nachricht.ts index 5a4a59cacb39eac54cb25676f61da3cf955508bd..48ef1b0a7fceb4ab0a3b727cc5ef4871e52d504c 100644 --- a/goofy-client/apps/goofy-e2e/src/model/postfach-nachricht.ts +++ b/goofy-client/apps/goofy-e2e/src/model/postfach-nachricht.ts @@ -5,6 +5,7 @@ export enum PostfachReplyOptionE2E { export class PostfachE2E { subject: string; mailBody: string; - replyOption: PostfachReplyOptionE2E + replyOption: PostfachReplyOptionE2E; + attachments: string[] | string; class: string; } \ No newline at end of file diff --git a/goofy-client/apps/goofy/src/styles/material/_expansion-panel.scss b/goofy-client/apps/goofy/src/styles/material/_expansion-panel.scss index ca51bf6cf68293982b26a3e9dbad73709eff6d8b..ffde46bcaca2210de52fc2cd74ccd31a86e43196 100644 --- a/goofy-client/apps/goofy/src/styles/material/_expansion-panel.scss +++ b/goofy-client/apps/goofy/src/styles/material/_expansion-panel.scss @@ -3,10 +3,11 @@ .mat-expansion-panel { display: flex; align-items: center; - height: 48px !important; + height: 44px !important; background-color: inherit; box-shadow: none; border-radius: 0; + font-size: 14px; } :host-context(.dark) .mat-expansion-panel { diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user-container.component.scss b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user-container.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..641aa7c05e3c3741793361763402aaad017bc9bf 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user-container.component.scss +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user-container.component.scss @@ -0,0 +1 @@ +@import "expansion-panel"; diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user/historie-item-assign-user.component.spec.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user/historie-item-assign-user.component.spec.ts index e5fc03b279ae1ed32ce57aa28e4de0baa50c3059..357aee0f2d73aa4bb6f86483952687f02fd60c98 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user/historie-item-assign-user.component.spec.ts +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user/historie-item-assign-user.component.spec.ts @@ -33,7 +33,7 @@ describe('HistorieItemAssignUserComponent', () => { it('should return "Unbekannter Benutzer" on user profile is null', () => { const headline: string = component.buildHeadline(null); - expect(headline).toBe('Unbekannter Benutzer'); + expect(headline).toBe('Unbekannter Benutzer den Vorgang zugewiesen.'); }) it('should return firstName lastName on user profile exists', () => { diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user/historie-item-assign-user.component.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user/historie-item-assign-user.component.ts index d9ddadeeddf06dddd6e0b1ef495c3c86f4c714f3..6891496e079d8fd8cd0c80f1852ba662d0202399 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user/historie-item-assign-user.component.ts +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-assign-user-container/historie-item-assign-user/historie-item-assign-user.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core'; import { CommandResource } from '@goofy-client/command-shared'; import { UserProfileResource } from '@goofy-client/user-profile-shared'; -import { isNull } from 'lodash-es'; +import { isNil } from 'lodash-es'; @Component({ selector: 'goofy-client-historie-item-assign-user', @@ -19,9 +19,10 @@ export class HistorieItemAssignUserComponent { headline: string; buildHeadline(userProfile: UserProfileResource): string { - if (isNull(userProfile)) { - return 'Unbekannter Benutzer'; - } - return `${userProfile.firstName} ${userProfile.lastName} den Vorgang zugewiesen.`; + const userName = isNil(userProfile) + ? 'Unbekannter Benutzer' + : `${userProfile.firstName} ${userProfile.lastName}`; + + return `${userName} den Vorgang zugewiesen.`; } } \ No newline at end of file diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.html b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.html new file mode 100644 index 0000000000000000000000000000000000000000..8c2d5350a240244c3cda857c76301e1c28af5785 --- /dev/null +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.html @@ -0,0 +1 @@ +<p data-test-class="historie-item-attachment">{{text}}</p> \ No newline at end of file diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.scss b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.spec.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..234f5b36ab316c71c5ac95b709a9d43fcd06eae0 --- /dev/null +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { faker } from '@faker-js/faker'; +import { HistorieItemAttachmentComponent } from './historie-item-attachment.component'; + +describe('HistorieItemAttachmentComponent', () => { + let component: HistorieItemAttachmentComponent; + let fixture: ComponentFixture<HistorieItemAttachmentComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [HistorieItemAttachmentComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HistorieItemAttachmentComponent); + component = fixture.componentInstance; + component.attachments = faker.datatype.uuid(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('build attachment text', () => { + + it('should return text for single attachment', () => { + const text: string = component.buildAttachmentText(faker.datatype.uuid()); + + expect(text).toBe('Es existiert ein Anhang zu diesem Eintrag.'); + }) + + it('should return text for multiple attachment', () => { + const text: string = component.buildAttachmentText([faker.datatype.uuid(), faker.datatype.uuid()]); + + expect(text).toBe(`Es existieren 2 Anhänge zu diesem Eintrag.`); + }) + }) +}); diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ad3ffedfa89c5822528e1c37fe4c68d877f3034 --- /dev/null +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-attachment/historie-item-attachment.component.ts @@ -0,0 +1,26 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { isString } from 'lodash-es'; + +@Component({ + selector: 'goofy-client-historie-item-attachment', + templateUrl: './historie-item-attachment.component.html', + styleUrls: ['./historie-item-attachment.component.scss'], +}) +export class HistorieItemAttachmentComponent implements OnInit { + + @Input() attachments: object | string; + + text: string; + + ngOnInit(): void { + this.text = this.buildAttachmentText(this.attachments); + } + + buildAttachmentText(attachments: any): string { + if (isString(attachments)) { + return 'Es existiert ein Anhang zu diesem Eintrag.' + } else { + return `Es existieren ${attachments.length} Anhänge zu diesem Eintrag.`; + } + } +} \ No newline at end of file diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.html b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.html index 7fb26516b70499eeeabcfa99905a7efd1470a27e..697e36d1c4f77b6ded5de17aac9c29e6f6cfd324 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.html +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.html @@ -1,2 +1,2 @@ -<goofy-client-user-profile-in-historie-container *ngIf="resource | hasLink: commandLinkRel.CREATED_BY; else nonUserCommand" [command]="resource" data-test-id="user-profile-in-historie-item-header"></goofy-client-user-profile-in-historie-container> -<ng-template #nonUserCommand><goofy-client-app-icon></goofy-client-app-icon> <span data-test-id="non-user-command-text">Die Anwendung</span></ng-template> <p class="headline" data-test-class="historie-item-header">hat am {{(resource.createdAt | formatDateWithTimePipe: false)}} {{headline}}</p> \ No newline at end of file +<goofy-client-user-profile-in-historie-container *ngIf="resource | hasLink: commandLinkRel.CREATED_BY; else nonUserCommand" [command]="resource" data-test-class="user-profile-in-historie-item-header"></goofy-client-user-profile-in-historie-container> +<ng-template #nonUserCommand><goofy-client-app-icon></goofy-client-app-icon><span data-test-class="system-user-in-historie-item-header">Die Anwendung</span></ng-template><p class="headline" data-test-class="historie-item-header">hat am {{(resource.createdAt | formatDateWithTimePipe: false)}} {{headline}}</p> \ No newline at end of file diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.scss b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.scss index 237872ac5be7cdf881420ce7bb3f28826e1a673c..65aab3277e4329882cb94dcd9c668b37c0709e5b 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.scss +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.scss @@ -2,6 +2,8 @@ display: flex; white-space: nowrap; align-items: center; + min-height: 44px; + font-size: 14px; } .headline { diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.spec.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.spec.ts index 1452d91605e50510ab14f1522062786638c43ff4..08b33bebf94e204ac5a8c12d4516ad75ca134ba3 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.spec.ts +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-header/historie-item-header.component.spec.ts @@ -7,7 +7,7 @@ import { AppIconComponent } from '@goofy-client/ui'; import { UserProfileInHistorieContainerComponent } from '@goofy-client/user-profile'; import { CommandLinkRel } from 'libs/command-shared/src/lib/command.linkrel'; import { createCommandResource } from 'libs/command-shared/test/command'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; import { MockComponent } from 'ng-mocks'; import { HistorieItemHeaderComponent } from './historie-item-header.component'; @@ -17,8 +17,8 @@ describe('HistorieItemHeaderComponent', () => { let component: HistorieItemHeaderComponent; let fixture: ComponentFixture<HistorieItemHeaderComponent>; - const userProfile: string = getDataTestIdOf('user-profile-in-historie-item-header'); - const nonUserCommandText: string = getDataTestIdOf('non-user-command-text'); + const userProfile: string = getDataTestClassOf('user-profile-in-historie-item-header'); + const nonUserCommandText: string = getDataTestClassOf('system-user-in-historie-item-header'); beforeEach(async () => { diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-kommentar/historie-item-kommentar.component.html b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-kommentar/historie-item-kommentar.component.html index 614cd77fe5f8d447ef24e8384e52e342f5b9337c..da776ec3f888011707cfe5b0ca0917026dcf369c 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-kommentar/historie-item-kommentar.component.html +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-kommentar/historie-item-kommentar.component.html @@ -1,3 +1,3 @@ <goofy-client-expansion-panel-with-user [headline]="headline" [resource]="command"> - <p>{{ kommentar.text }}</p> + <p data-test-class="kommentar-text">{{ kommentar.text }}</p> </goofy-client-expansion-panel-with-user> \ No newline at end of file diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.html b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.html index d79adc29a2a6506ac9075254c004732d85d48576..b88f0973a260e616acfe051463168af9a15a51b4 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.html +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.html @@ -1,4 +1,5 @@ <goofy-client-expansion-panel-with-user [headline]="headline" [resource]="command" data-test-id="historie-item-wiedervorlage-user-expansion-panel"> <p data-test-class="postfach-nachricht-subject" class="subject">{{postfachNachricht.subject}}</p> <p data-test-class="postfach-nachricht-mail-body">{{postfachNachricht.mailBody}}</p> + <goofy-client-historie-item-attachment *ngIf="postfachNachricht.attachments" [attachments]="postfachNachricht.attachments" data-test-id="historie-item-postfach-nachricht-attachment"></goofy-client-historie-item-attachment> </goofy-client-expansion-panel-with-user> \ No newline at end of file diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.spec.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.spec.ts index 944cd15d6b9c79dbeb920998e8ef5d761ae5b776..f946aa05a919f41b867597f5b4e4d25cc4584e21 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.spec.ts +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.spec.ts @@ -1,10 +1,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CommandOrder, CommandResource } from '@goofy-client/command-shared'; import { PostfachMail } from '@goofy-client/postfach-shared'; +import { getElementFromFixture } from '@goofy-client/test-utils'; import { createCommandResource } from 'libs/command-shared/test/command'; import { createPostfachMail } from 'libs/postfach-shared/test/postfach'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent } from 'ng-mocks'; import { ExpansionPanelWithUserComponent } from '../expansion-panel-with-user/expansion-panel-with-user.component'; +import { HistorieItemAttachmentComponent } from '../historie-item-attachment/historie-item-attachment.component'; import { HistorieItemPostfachNachrichtComponent } from './historie-item-postfach-nachricht.component'; describe('HistorieItemPostfachNachrichtComponent', () => { @@ -14,11 +17,14 @@ describe('HistorieItemPostfachNachrichtComponent', () => { const item: PostfachMail = createPostfachMail(); const postfachNachrichtCommand: CommandResource = { ...createCommandResource(), body: { item: item } }; + const attachment: string = getDataTestIdOf('historie-item-postfach-nachricht-attachment'); + beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ HistorieItemPostfachNachrichtComponent, - MockComponent(ExpansionPanelWithUserComponent) + MockComponent(ExpansionPanelWithUserComponent), + MockComponent(HistorieItemAttachmentComponent) ], }).compileComponents(); }); @@ -83,4 +89,27 @@ describe('HistorieItemPostfachNachrichtComponent', () => { }) }) }) + + describe('attachments', () => { + + it('should hide of NOT exists', () => { + const item: PostfachMail = createPostfachMail(); + delete item['attachments']; + component.postfachNachricht = item; + fixture.detectChanges(); + + const attachmentElement = getElementFromFixture(fixture, attachment); + + expect(attachmentElement).not.toBeInstanceOf(HTMLElement); + }) + + it('should show if exists', () => { + component.postfachNachricht = createPostfachMail(); + fixture.detectChanges(); + + const attachmentElement = getElementFromFixture(fixture, attachment); + + expect(attachmentElement).toBeInstanceOf(HTMLElement); + }) + }) }); diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.ts index 38192775e12f51dae3085f24311d931572f23f1b..18e1f2b4cd0e0b1579e7488f4653370f2252e1e4 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.ts +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-postfach-nachricht/historie-item-postfach-nachricht.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { CommandOrder, CommandResource } from '@goofy-client/command-shared'; -import { PostfachMailResource } from '@goofy-client/postfach-shared'; +import { PostfachMail } from '@goofy-client/postfach-shared'; @Component({ selector: 'goofy-client-historie-item-postfach-nachricht', @@ -12,14 +12,14 @@ export class HistorieItemPostfachNachrichtComponent { @Input() command: CommandResource; headline: string; - postfachNachricht: PostfachMailResource; + postfachNachricht: PostfachMail; ngOnInit(): void { this.headline = HISTORIE_TEXT_BY_POSTFACH_NACHRICHT_ORDER[this.command.order]; this.postfachNachricht = this.getPostfachNachricht(); } - getPostfachNachricht(): PostfachMailResource { + getPostfachNachricht(): PostfachMail { if (this.command.order == CommandOrder.SEND_POSTFACH_NACHRICHT) { return this.command.body; } diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-vorgang-created/historie-item-vorgang-created.component.scss b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-vorgang-created/historie-item-vorgang-created.component.scss index 028fa20ffcaaf8f38c2c42ab1c7c645ff141382e..1700a5173c7bf57eeb0f3f986d818dc2220d753e 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-vorgang-created/historie-item-vorgang-created.component.scss +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-vorgang-created/historie-item-vorgang-created.component.scss @@ -5,6 +5,8 @@ border-bottom: 1px solid $greyLight; display: block; margin-top: 0.5rem; + min-height: 44px; + font-size: 14px; } p { diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.html b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.html new file mode 100644 index 0000000000000000000000000000000000000000..79feecbc760680c58e075d91ba325f4b9cb0deac --- /dev/null +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.html @@ -0,0 +1 @@ +<p data-test-class="wiedervorlage-status">{{text}}</p> \ No newline at end of file diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.scss b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.spec.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c12f451a5660b563d5b4dea03835f22aa2783aee --- /dev/null +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HistorieItemWiedervorlageStatusComponent } from './historie-item-wiedervorlage-status.component'; + +describe('HistorieItemWiedervorlageStatusComponent', () => { + let component: HistorieItemWiedervorlageStatusComponent; + let fixture: ComponentFixture<HistorieItemWiedervorlageStatusComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [HistorieItemWiedervorlageStatusComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent( + HistorieItemWiedervorlageStatusComponent + ); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('set status text for "done" value', () => { + + it('should return "offen" on false', () => { + component.done = 'false'; + + expect(component.text).toBe('offen'); + }) + + it('should return "geschlossen" on true', () => { + component.done = 'true'; + + expect(component.text).toBe('geschlossen'); + }) + }) +}); diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1febc2ec57195c906415045683e050776a088bb4 --- /dev/null +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'goofy-client-historie-item-wiedervorlage-status', + templateUrl: './historie-item-wiedervorlage-status.component.html', + styleUrls: ['./historie-item-wiedervorlage-status.component.scss'], +}) +export class HistorieItemWiedervorlageStatusComponent { + + @Input() + public set done(done: string) { + this.text = Boolean(JSON.parse(done)) ? 'geschlossen' : 'offen'; + } + + text: string; +} diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component.html b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component.html index cf4caa9631b185f4503158e6201892bbe84b33d1..cd2111ec7199a8155aa79069a31fcdf991d11361 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component.html +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component.html @@ -1,9 +1,11 @@ -<goofy-client-expansion-panel-with-user *ngIf="hasBody; else headlineOnly" [headline]="headline" [resource]="command" data-test-id="historie-item-wiedervorlage-user-expansion-panel"> - <p><span class="betreff">{{wiedervorlage.betreff}}</span>, {{wiedervorlage.frist | formatToPrettyDate}}</p> - <p>{{wiedervorlage.beschreibung}}</p> +<goofy-client-expansion-panel-with-user *ngIf="hasBody; else headlineOnly" [headline]="headline" [resource]="command" data-test-class="historie-item-wiedervorlage-user-expansion-panel"> + <goofy-client-historie-item-wiedervorlage-status [done]="wiedervorlage.done"></goofy-client-historie-item-wiedervorlage-status> + <p><span class="betreff" data-test-class="wiedervorlage-betreff">{{wiedervorlage.betreff}}</span>, {{wiedervorlage.frist | formatToPrettyDate}}</p> + <p data-test-class="wiedervorlage-beschreibung">{{wiedervorlage.beschreibung}}</p> + <goofy-client-historie-item-attachment *ngIf="wiedervorlage.attachments" [attachments]="wiedervorlage.attachments" data-test-class="historie-item-wiedervorlage-attachment"></goofy-client-historie-item-attachment> </goofy-client-expansion-panel-with-user> <ng-template #headlineOnly> <div class="mat-expansion-panel"> - <goofy-client-historie-item-header [headline]="headline" [resource]="command" data-test-id="historie-item-wiedervorlage-header"></goofy-client-historie-item-header> + <goofy-client-historie-item-header [headline]="headline" [resource]="command" data-test-class="historie-item-wiedervorlage-header"></goofy-client-historie-item-header> </div> </ng-template> \ No newline at end of file diff --git a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component.spec.ts b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component.spec.ts index 589060777f83e4ebf4981c6352e20b0205054e80..2ef06f603157c4277b8ff113e1a34564e3bc794b 100644 --- a/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component.spec.ts +++ b/goofy-client/libs/historie/src/lib/historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component.spec.ts @@ -5,11 +5,13 @@ import { CommandOrder, CommandResource } from '@goofy-client/command-shared'; import { FormatToPrettyDatePipe } from '@goofy-client/tech-shared'; import { getElementFromFixture } from '@goofy-client/test-utils'; import { createCommandResource } from 'libs/command-shared/test/command'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; import { createWiedervorlageResource } from 'libs/wiedervorlage-shared/test/wiedervorlage'; import { MockComponent } from 'ng-mocks'; import { ExpansionPanelWithUserComponent } from '../expansion-panel-with-user/expansion-panel-with-user.component'; +import { HistorieItemAttachmentComponent } from '../historie-item-attachment/historie-item-attachment.component'; import { HistorieItemHeaderComponent } from '../historie-item-header/historie-item-header.component'; +import { HistorieItemWiedervorlageStatusComponent } from './historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component'; import { HistorieItemWiedervorlageComponent } from './historie-item-wiedervorlage.component'; registerLocaleData(localeDe); @@ -26,8 +28,9 @@ describe('HistorieItemWiedervorlageComponent', () => { } }; - const userExpansionPanel: string = getDataTestIdOf('historie-item-wiedervorlage-user-expansion-panel'); - const itemHeadline: string = getDataTestIdOf('historie-item-wiedervorlage-header'); + const userExpansionPanel: string = getDataTestClassOf('historie-item-wiedervorlage-user-expansion-panel'); + const itemHeadline: string = getDataTestClassOf('historie-item-wiedervorlage-header'); + const attachment: string = getDataTestClassOf('historie-item-wiedervorlage-attachment'); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -35,7 +38,9 @@ describe('HistorieItemWiedervorlageComponent', () => { FormatToPrettyDatePipe, HistorieItemWiedervorlageComponent, MockComponent(HistorieItemHeaderComponent), - MockComponent(ExpansionPanelWithUserComponent) + MockComponent(ExpansionPanelWithUserComponent), + MockComponent(HistorieItemWiedervorlageStatusComponent), + MockComponent(HistorieItemAttachmentComponent) ], }).compileComponents(); }); @@ -127,6 +132,29 @@ describe('HistorieItemWiedervorlageComponent', () => { const userPanel = getElementFromFixture(fixture, itemHeadline); expect(userPanel).not.toBeInstanceOf(HTMLElement); }) + + describe('attachments', () => { + + it('should hide of NOT exists', () => { + const item = createWiedervorlageResource(); + delete item['attachments']; + component.command = { ...createCommandResource(), order: CommandOrder.CREATE_WIEDERVORLAGE, body: { item } }; + fixture.detectChanges(); + + const attachmentElement = getElementFromFixture(fixture, attachment); + + expect(attachmentElement).not.toBeInstanceOf(HTMLElement); + }) + + it('should show if exists', () => { + component.command = { ...createCommandResource(), order: CommandOrder.CREATE_WIEDERVORLAGE, body: { item: { ...createWiedervorlageResource(), attachment: '' } } }; + fixture.detectChanges(); + + const attachmentElement = getElementFromFixture(fixture, attachment); + + expect(attachmentElement).toBeInstanceOf(HTMLElement); + }) + }) }) describe('on erledigen/wiedereroeffnen order', () => { diff --git a/goofy-client/libs/historie/src/lib/historie.module.ts b/goofy-client/libs/historie/src/lib/historie.module.ts index 1ebca80f7157fda8eeba58e54cc44fa2c3fe63ed..0eaececa21eb118dd5668ed29b5c6b0a98f6f56c 100644 --- a/goofy-client/libs/historie/src/lib/historie.module.ts +++ b/goofy-client/libs/historie/src/lib/historie.module.ts @@ -19,6 +19,8 @@ import { HistorieItemVorgangStatusComponent } from './historie-container/histori import { HistorieItemWiedervorlageComponent } from './historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage.component'; import { HistorieListItemComponent } from './historie-container/historie-list/historie-list-item/historie-list-item.component'; import { HistorieListComponent } from './historie-container/historie-list/historie-list.component'; +import { HistorieItemWiedervorlageStatusComponent } from './historie-container/historie-list/historie-item-wiedervorlage/historie-item-wiedervorlage-status/historie-item-wiedervorlage-status.component'; +import { HistorieItemAttachmentComponent } from './historie-container/historie-list/historie-item-attachment/historie-item-attachment.component'; @NgModule({ imports: [ @@ -44,7 +46,9 @@ import { HistorieListComponent } from './historie-container/historie-list/histor HistorieItemForwardingComponent, HistorieItemAssignUserComponent, HistorieItemAssignUserContainerComponent, + HistorieItemWiedervorlageStatusComponent, + HistorieItemAttachmentComponent, ], exports: [HistorieContainerComponent], }) -export class HistorieModule { } +export class HistorieModule {} diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.actions.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.actions.ts index c08959ca2980c1e579f11e8182cbc71af6480181..d1fbee74e909808ad0677a9a65a3a8f575d10694 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.actions.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.actions.ts @@ -1,5 +1,6 @@ import { createAction, props } from '@ngrx/store'; -import { CurrentRouteData } from './navigation.models'; +import { RouteData } from './navigation.models'; -export const updateRouteData = createAction('[RouteData] Update...', props<{ routeData: CurrentRouteData }>()); -export const noOp = createAction('[RouteData] No Operation');//TOTHINK auf [System] umstellen? \ No newline at end of file +export const updateCurrentRouteData = createAction('[Navigation] Update current route data', props<{ routeData: RouteData }>()); + +export const noOp = createAction('[Navigation] No Operation'); \ No newline at end of file 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 78e131e8bbefb7d7492bfaaf4fcf221281ff4025..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 @@ -6,10 +6,10 @@ import { provideMockStore } from '@ngrx/store/testing'; import { NxModule } from '@nrwl/angular'; import { hot } from 'jest-marbles'; import { Observable } from 'rxjs'; -import { createCurrentRouteData } from './../../../test/navigation-test-factory'; +import * as NavigationUtil from '../navigation.util'; +import { createRouteData } from './../../../test/navigation-test-factory'; import * as NavigationActions from './navigation.actions'; import { NavigationEffects } from './navigation.effects'; -import { CurrentRouteData } from './navigation.models'; describe('NavigationEffects', () => { let actions: Observable<Action>; @@ -30,17 +30,17 @@ describe('NavigationEffects', () => { describe('navigate$', () => { - it.skip('FIXME: should dispatch updateRouteData action with data', async () => { - actions = hot('-a-|', { a: routerNavigatedAction }); + const action = routerNavigatedAction; - const routeData: CurrentRouteData = createCurrentRouteData(); - const expected = hot('-a-|', { a: NavigationActions.updateRouteData({ routeData }) }); + it('should dispatch updateCurrentRouteData action with data', () => { + jest.spyOn(NavigationUtil, 'buildRouteData').mockReturnValue(createRouteData()); + actions = hot('-a-|', { a: action }); - expect(effects.navigate$).toBeObservable(expected); - /* effects.navigate$.subscribe(response => { - expect(response.type).toEqual(NavigationActions.updateRouteData.type); - expect(response.routeData).toEqual(routeData); - }) */ + effects.navigateEnd$.subscribe(); + + const expected = hot('-a-|', { a: NavigationActions.updateCurrentRouteData({ routeData: createRouteData() }) }); + + expect(effects.navigateEnd$).toBeObservable(expected); }) }) -}); +}); \ No newline at end of file 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 4f929602fd67adf895c186670f0a6495d80c5fe9..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,31 +1,22 @@ import { Injectable } from '@angular/core'; -import { isNotUndefined } from '@goofy-client/tech-shared'; 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'; import * as NavigationActions from './navigation.actions'; -import { CurrentRouteData } from './navigation.models'; @Injectable() export class NavigationEffects { constructor(private readonly actions$: Actions) { } - navigate$ = createEffect(() => + navigateEnd$ = createEffect(() => this.actions$.pipe( - ofType(routerNavigationAction), - switchMap((action) => of(NavigationActions.updateRouteData({ routeData: this.buildCurrentRouteData(action) }))) + ofType(routerNavigatedAction), + switchMap((action) => { + return of(NavigationActions.updateCurrentRouteData({ routeData: buildRouteData(action) })) + }) ) ) - private buildCurrentRouteData(action: any): CurrentRouteData {//TODO Typisieren!? - const root = action.payload.routerState.root; - const lastFirst = this.getLastFirstChild(root) - return { queryParameter: lastFirst.params, urlSegments: lastFirst.url }; - } - - private getLastFirstChild(route: any) { - while (isNotUndefined(route.firstChild)) route = route.firstChild; - return route; - } } \ No newline at end of file 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 c03bdcae0a635dbf48f0120c12819bf802f9a585..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 { CurrentRouteData } from './navigation.models'; -import { NavigationPartialState, NAVIGATION_FEATURE_KEY, reducer } from './navigation.reducer'; +import { RouteData } from './navigation.models'; 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: CurrentRouteData = 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.facade.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.facade.ts index d6aab47eb9d2999ae683ae1cb7056de337f198e4..40761ff574d265e0651cba7ae8fad918bcb36d01 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.facade.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.facade.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { CurrentRouteData } from './navigation.models'; +import { RouteData } from './navigation.models'; import * as NavigationSelectors from './navigation.selectors'; @Injectable() @@ -9,7 +9,7 @@ export class NavigationFacade { constructor(private readonly store: Store) { } - public getCurrentRouteData(): Observable<CurrentRouteData> { + public getCurrentRouteData(): Observable<RouteData> { return this.store.select(NavigationSelectors.currentRouteData); } } \ No newline at end of file diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.models.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.models.ts index f302bc5736ea2ea4d32d896da04632597b4d5822..1a75995ff82fa32ad2783240844d8c0c1eebc025 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.models.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.models.ts @@ -1,6 +1,6 @@ import { UrlSegment } from '@angular/router'; -export interface CurrentRouteData { +export interface RouteData { urlSegments: UrlSegment[], queryParameter: any } \ 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 0c392a72ca5b6141e6cd2400f8a4d0009c806e24..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,6 +1,6 @@ -import { createCurrentRouteData } from './../../../test/navigation-test-factory'; +import { createRouteData } from './../../../test/navigation-test-factory'; import * as NavigationActions from './navigation.actions'; -import { CurrentRouteData } from './navigation.models'; +import { RouteData } from './navigation.models'; import { initialState, NavigationState, reducer } from './navigation.reducer'; describe('Navigation Reducer', () => { @@ -8,12 +8,12 @@ describe('Navigation Reducer', () => { describe('on get updateRouteData action', () => { it('should set route data', () => { - const routeData: CurrentRouteData = createCurrentRouteData(); - const action = NavigationActions.updateRouteData({ routeData }); + 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 3e4e8a6f6297a63cce9d2e48fa260f190c094a04..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 @@ -1,29 +1,29 @@ import { Action, createReducer, on } from '@ngrx/store'; import * as NavigationActions from './navigation.actions'; -import { CurrentRouteData } from './navigation.models'; +import { RouteData } from './navigation.models'; export const NAVIGATION_FEATURE_KEY = 'NavigationState'; -export interface NavigationState { - currentRouteData: CurrentRouteData | null -} - export interface NavigationPartialState { readonly [NAVIGATION_FEATURE_KEY]: NavigationState; } +export interface NavigationState { + currentRouteData: RouteData +} + export const initialState: NavigationState = { - currentRouteData: null, + currentRouteData: null }; const navigationReducer = createReducer( initialState, - on(NavigationActions.updateRouteData, (state: NavigationState, { routeData }) => ({ + on(NavigationActions.updateCurrentRouteData, (state: NavigationState, { routeData }) => ({ ...state, currentRouteData: routeData })) ); -export function reducer(state: NavigationState | undefined, action: Action) { +export function reducer(state: NavigationState, action: Action) { return navigationReducer(state, action); } \ No newline at end of file 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 72340f5ff20905711a4718b3ca09aad486363f3c..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,5 +1,5 @@ -import { createCurrentRouteData } from './../../../test/navigation-test-factory'; -import { CurrentRouteData } from './navigation.models'; +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: CurrentRouteData = createCurrentRouteData(); + const currentRouteData: RouteData = createRouteData(); beforeEach(() => { state = { @@ -19,8 +19,6 @@ describe('Navigation Selectors', () => { }); it('should return currentRouteData', () => { - const result = NavigationSelectors.currentRouteData(state); - - expect(result).toBe(currentRouteData); + expect(NavigationSelectors.currentRouteData.projector(state.NavigationState)).toBe(currentRouteData); }) }) \ No newline at end of file diff --git a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.selectors.ts b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.selectors.ts index 709716066d0f383ee5c4cd47b351d77bd462dea8..38f2a6f2acbff36682361ae9e9afa45880ad27ab 100644 --- a/goofy-client/libs/navigation-shared/src/lib/+state/navigation.selectors.ts +++ b/goofy-client/libs/navigation-shared/src/lib/+state/navigation.selectors.ts @@ -1,8 +1,7 @@ -import { createFeatureSelector, createSelector } from '@ngrx/store'; +import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; +import { RouteData } from './navigation.models'; import { NavigationState, NAVIGATION_FEATURE_KEY } from './navigation.reducer'; -export const getNavigationState = createFeatureSelector<NavigationState>(NAVIGATION_FEATURE_KEY); +export const getNavigationState: MemoizedSelector<object, NavigationState> = createFeatureSelector<NavigationState>(NAVIGATION_FEATURE_KEY); -export const currentRouteData = createSelector(getNavigationState, (state: NavigationState) => { - return state.currentRouteData; -}) \ No newline at end of file +export const currentRouteData: MemoizedSelector<NavigationState, RouteData> = createSelector(getNavigationState, (state: NavigationState) => state.currentRouteData); \ No newline at end of file diff --git a/goofy-client/libs/navigation-shared/src/lib/navigation.util.ts b/goofy-client/libs/navigation-shared/src/lib/navigation.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..eafc0c4dee03acd6656820db33ba02207a38125f --- /dev/null +++ b/goofy-client/libs/navigation-shared/src/lib/navigation.util.ts @@ -0,0 +1,13 @@ +import { isNotUndefined } from '@goofy-client/tech-shared'; +import { RouteData } from './+state/navigation.models'; + +export function buildRouteData(action: any): RouteData { + const root = action.payload.routerState.root; + const lastFirst = getLastFirstChild(root) + return { queryParameter: lastFirst.params, urlSegments: lastFirst.url }; +} + +function getLastFirstChild(route: any) { + while (isNotUndefined(route.firstChild)) route = route.firstChild; + return route; +} \ No newline at end of file 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 0dc0f7a59627e7f066dd4ee06a356e4a3e91e691..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 { CurrentRouteData } from '../src/lib/+state/navigation.models'; +import { RouteData } from '../src/lib/+state/navigation.models'; -export function createCurrentRouteData(): CurrentRouteData { +export function createRouteData(): RouteData { return { queryParameter: {}, urlSegments: [] }; } \ No newline at end of file diff --git a/goofy-client/libs/postfach-shared/src/lib/postfach.model.ts b/goofy-client/libs/postfach-shared/src/lib/postfach.model.ts index 16b14da973576f667d55f47b6a9e35ac31e94115..daef45c4fdf916f3de4efa15c61c5fa9d06fbd10 100644 --- a/goofy-client/libs/postfach-shared/src/lib/postfach.model.ts +++ b/goofy-client/libs/postfach-shared/src/lib/postfach.model.ts @@ -12,7 +12,7 @@ export interface PostfachMail { sentAt: Date; sentSuccessful: boolean; messageCode: PostfachNachrichtMessageCode; - attachments: ResourceUri[]; + attachments: ResourceUri[] | string; } export enum Direction { diff --git a/goofy-client/libs/postfach-shared/test/postfach.ts b/goofy-client/libs/postfach-shared/test/postfach.ts index 0e6c9f56373b8a5b5bd9b2d138203f37fd1dabfd..000203406ea48ad9e097ff9cceff6c3ae1b7d04a 100644 --- a/goofy-client/libs/postfach-shared/test/postfach.ts +++ b/goofy-client/libs/postfach-shared/test/postfach.ts @@ -15,7 +15,7 @@ export function createPostfachMail(): PostfachMail { sentAt: faker.date.past(), sentSuccessful: false, messageCode: PostfachNachrichtMessageCode.CONNECTION_FAILED, - attachments: [] + attachments: faker.datatype.uuid() } } diff --git a/goofy-client/libs/tech-shared/src/lib/resource/resource.util.ts b/goofy-client/libs/tech-shared/src/lib/resource/resource.util.ts index 32218e209a4b096c432d4ceb4717235e1c23b0a2..2ff7abbcb2f285d5dc85bedf50def19f62d6df40 100644 --- a/goofy-client/libs/tech-shared/src/lib/resource/resource.util.ts +++ b/goofy-client/libs/tech-shared/src/lib/resource/resource.util.ts @@ -24,7 +24,7 @@ export function createStateResource<T>(resource: T, loading: boolean = false): S } export function createErrorStateResource<T>(error: ApiError): StateResource<any> { - return { ...createEmptyStateResource<T>(), error }; + return { ...createEmptyStateResource<T>(), error, loaded: true }; } export function doIfLoadingRequired(stateResource: StateResource<any>, runable: () => void): boolean { diff --git a/goofy-client/libs/tech-shared/test/error.ts b/goofy-client/libs/tech-shared/test/error.ts index 19f1a285a9df45b631142840c0e4450d516d72d6..3205a1a61e418468636d6930aeb4780070435ab6 100644 --- a/goofy-client/libs/tech-shared/test/error.ts +++ b/goofy-client/libs/tech-shared/test/error.ts @@ -22,3 +22,8 @@ export function createApiError(): ApiError { issues: [createIssue()] }; } + +//TODO typisieren -> wirkt sich entsprechend auf die actions und den reducer/state aus +export function createError(): unknown { + return {}; +} \ No newline at end of file diff --git a/goofy-client/libs/test-utils/src/lib/helper.ts b/goofy-client/libs/test-utils/src/lib/helper.ts index 94887c81370ac5a428485249e842ddc34158afdd..7c775584cf72b5bd3546ab540fdc24deb7661f68 100644 --- a/goofy-client/libs/test-utils/src/lib/helper.ts +++ b/goofy-client/libs/test-utils/src/lib/helper.ts @@ -14,6 +14,10 @@ export function getElementFromFixture(fixture: ComponentFixture<any>, htmlElemen return fixture.nativeElement.querySelector(htmlElement); } +export function getElementsFromFixture(fixture: ComponentFixture<any>, htmlElement: string): any { + return fixture.nativeElement.querySelectorAll(htmlElement); +} + export function dispatchEventFromFixture(fixture: ComponentFixture<any>, elementSelector: string, event: string): void { const element = getDebugElementFromFixtureByCss(fixture, elementSelector) element.nativeElement.dispatchEvent(new Event(event)); diff --git a/goofy-client/libs/ui/src/lib/ui/expansion-panel/_expansion-panel.theme.scss b/goofy-client/libs/ui/src/lib/ui/expansion-panel/_expansion-panel.theme.scss index ba1b0b079bea0b4739db092171bfb321807c8157..d435bf4bdaedfde47c7298847b527fc01755c63f 100644 --- a/goofy-client/libs/ui/src/lib/ui/expansion-panel/_expansion-panel.theme.scss +++ b/goofy-client/libs/ui/src/lib/ui/expansion-panel/_expansion-panel.theme.scss @@ -14,7 +14,7 @@ body.mat-typography goofy-client-expansion-panel { .mat-expansion-panel-header { padding: 0 !important; - height: 48px !important; + height: 44px !important; } h3 { diff --git a/goofy-client/libs/ui/src/lib/ui/file-upload/file-upload.component.ts b/goofy-client/libs/ui/src/lib/ui/file-upload/file-upload.component.ts index 5b551b1588bec5db1f2851297096568b2dbc918d..99ec8be646735585390f55bb1c1c7d7cfa659320 100644 --- a/goofy-client/libs/ui/src/lib/ui/file-upload/file-upload.component.ts +++ b/goofy-client/libs/ui/src/lib/ui/file-upload/file-upload.component.ts @@ -17,7 +17,7 @@ export class FileUploadComponent { readonly myId: string = uniqueId(); onFileChanged($event: Event): void { - const files = (<HTMLInputElement>$event.target).files; + const files: FileList = (<HTMLInputElement>$event.target).files; this.fileChanged.emit(files[0]); } } \ No newline at end of file diff --git a/goofy-client/libs/user-profile-shared/src/lib/user-profile.service.ts b/goofy-client/libs/user-profile-shared/src/lib/user-profile.service.ts index 743e665b2f017b53cce9f647906aed17fe4e456f..833069c79479588526257058ed0e80cf78c8fc25 100644 --- a/goofy-client/libs/user-profile-shared/src/lib/user-profile.service.ts +++ b/goofy-client/libs/user-profile-shared/src/lib/user-profile.service.ts @@ -15,7 +15,7 @@ import { UserProfileRepository } from './user-profile.repository'; @Injectable({ providedIn: 'root' }) export class UserProfileService { - private userProfiles = <any>{}; + private userProfiles = {}; private userProfileSearchList: BehaviorSubject<StateResource<UserProfileListResource>> = new BehaviorSubject(createEmptyStateResource<UserProfileListResource>()); private userProfileSearchVisibility: BehaviorSubject<boolean> = new BehaviorSubject(false); @@ -34,10 +34,10 @@ export class UserProfileService { onNavigation(params: Params): void { if (NavigationService.isVorgangListPage(params)) { - this.userProfiles = <any>{}; + this.userProfiles = {}; this.hideUserProfileSearch(); } else if (NavigationService.isVorgangDetailPage(params, VorgangService.VORGANG_WITH_EINGANG_URL)) { - this.userProfiles = <any>{}; + this.userProfiles = {}; } } 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 a397e997c9677ed4d2b20e152b283d9428524680..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 @@ -1,12 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ApiRootFacade, ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-shared'; -import { createStateResource, HasLinkPipe, StateResource } from '@goofy-client/tech-shared'; +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 { 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'; -import { BehaviorSubject } from 'rxjs'; +import { of } from 'rxjs'; import { VorgangSearchContainerComponent } from './vorgang-search-container.component'; import { VorgangSearchComponent } from './vorgang-search/vorgang-search.component'; @@ -14,8 +14,7 @@ describe('VorgangSearchContainerComponent', () => { let component: VorgangSearchContainerComponent; let fixture: ComponentFixture<VorgangSearchContainerComponent>; - const apiRootSubj: BehaviorSubject<StateResource<ApiRootResource>> = new BehaviorSubject(createStateResource(createApiRootResource())); - const apiRootFacade = { ...mock(ApiRootFacade), getApiRoot: () => apiRootSubj }; + const apiRootFacade = { ...mock(ApiRootFacade), getApiRoot: jest.fn() }; const vorgangListService = mock(VorgangListService); @@ -51,10 +50,25 @@ describe('VorgangSearchContainerComponent', () => { expect(component).toBeTruthy(); }); + describe('ngOnInit', () => { + + it('should call apiRootFacade to get apiRoot', () => { + component.ngOnInit(); + + expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); + }) + + it('should call vorgangFacade to get searchPreviewList', () => { + component.ngOnInit(); + + expect(vorgangListService.getSearchPreviewList).toHaveBeenCalled(); + }) + }) + describe('vorgang-search', () => { it('should hide on no link exists', () => { - apiRootSubj.next(createStateResource(createApiRootResource())); + component.apiRoot$ = of(createStateResource(createApiRootResource())); fixture.detectChanges(); const element = fixture.nativeElement.querySelector(vorgangSearch); @@ -64,7 +78,7 @@ describe('VorgangSearchContainerComponent', () => { it.each([ApiRootLinkRel.SEARCH, ApiRootLinkRel.SEARCH_MY_VORGAENGE]) ('should show on link "%s"', (linkRel: string) => { - apiRootSubj.next(createStateResource(createApiRootResource([linkRel]))); + component.apiRoot$ = of(createStateResource(createApiRootResource([linkRel]))); fixture.detectChanges(); const element = fixture.nativeElement.querySelector(vorgangSearch); 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 7252d353c2c0ed720b99f7e1cdafd47d61ba03d8..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,4 +1,4 @@ -import { Component } from '@angular/core'; +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 { VorgangListResource, VorgangListService } from '@goofy-client/vorgang-shared'; @@ -9,15 +9,17 @@ import { Observable, of } from 'rxjs'; templateUrl: './vorgang-search-container.component.html', styleUrls: ['./vorgang-search-container.component.scss'] }) -export class VorgangSearchContainerComponent { +export class VorgangSearchContainerComponent implements OnInit { public apiRoot$: Observable<StateResource<ApiRootResource>> = of(createEmptyStateResource<ApiRootResource>()); public vorgangSearchPreviewList$: Observable<StateResource<VorgangListResource>> = of(createEmptyStateResource<VorgangListResource>()); readonly apiRootLinkRel = ApiRootLinkRel; - constructor(private apiRootFacade: ApiRootFacade, private vorgangListService: VorgangListService) { + constructor(private apiRootFacade: ApiRootFacade, private vorgangListService: VorgangListService) { } + + ngOnInit(): void { this.apiRoot$ = this.apiRootFacade.getApiRoot(); - this.vorgangSearchPreviewList$ = this.vorgangListService.getVorgangSearchPreviewList(); + 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 1740b68b68e49f777847c2cdac81bafcdd3d37ad..44b3aaf136bbf95cd471152ddf95a9120ffcdee2 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 @@ -26,7 +26,7 @@ describe('VorgangSearchComponent', () => { const searchFormService = mock(VorgangSearchFormService); const searchInfoSubj: Subject<SearchInfo> = new BehaviorSubject({ searchString: EMPTY_STRING, changedAfterSearchDone: false }); - const listService = { ...mock(VorgangListService), getSearchInfo: () => searchInfoSubj }; + const vorgangListService = { ...mock(VorgangListService), getSearchInfo: () => searchInfoSubj }; const searchPreviewOption: string = getDataTestClassOf('search-preview-option'); @@ -58,7 +58,7 @@ describe('VorgangSearchComponent', () => { }, { provide: VorgangListService, - useValue: listService + 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 68f3ce252a973cf63fc539f3c4bb5280ccb7cbd6..9fd68c327d46104e80f30e86756d71bbc26ce3b0 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 @@ -11,8 +11,8 @@ import { VorgangSearchFormService } from './vorgang-search.formservice'; describe('VorgangSearchFormService', () => { let formService: VorgangSearchFormService;; - let vorgangListService: Mock<VorgangListService>; let navigationService: Mock<NavigationService>; + let vorgangListService: Mock<VorgangListService> const SEARCH_STRING = 'i search for...'; @@ -21,7 +21,7 @@ describe('VorgangSearchFormService', () => { vorgangListService.getSearchInfo.mockReturnValue(of({})); navigationService = mock(NavigationService); - formService = new VorgangSearchFormService(useFromMock(vorgangListService), new UntypedFormBuilder(), useFromMock(navigationService)); + formService = new VorgangSearchFormService(new UntypedFormBuilder(), useFromMock(navigationService), useFromMock(vorgangListService)); }) it('should create', () => { @@ -47,12 +47,28 @@ describe('VorgangSearchFormService', () => { 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(vorgangListService.clearVorgangSearchPreviewList).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(); }) }) @@ -70,7 +86,7 @@ describe('VorgangSearchFormService', () => { it('should call submit for preview list', () => { formService.clearVorgangSearchPreviewList(); - expect(vorgangListService.clearVorgangSearchPreviewList).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 2841e30e48b2118ab7ba66b2f1e9369a802d6a42..3cc7131b19360a684cafe58c30d7292b4618c80b 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 @@ -12,7 +12,7 @@ export class VorgangSearchFormService implements OnDestroy { form: UntypedFormGroup; - readonly SEARCH_FIELD: string = VorgangListService.SEARCH; + readonly SEARCH_FIELD: string = 'search'; readonly PREVIEW_SEARCH_STRING_MIN_LENGTH = 3; private subscription: Subscription; @@ -21,9 +21,9 @@ export class VorgangSearchFormService implements OnDestroy { searchLocked: boolean; constructor( - private service: VorgangListService, private formBuilder: UntypedFormBuilder, private navigationService: NavigationService, + 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.service.searchForPreview(searchInput); + this.vorgangListService.searchForPreview(searchInput); } clearVorgangSearchPreviewList(): void { - this.service.clearVorgangSearchPreviewList(); + this.vorgangListService.clearSearchPreviewList(); } private subscribeToSearchString(): void { - this.subscription = this.service.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); } diff --git a/goofy-client/libs/vorgang-shared/src/index.ts b/goofy-client/libs/vorgang-shared/src/index.ts index b14acde4f4ca614844a41c28b6affc90220b46a5..d3cad9fd2daca488055be15aaba03d987bfad448 100644 --- a/goofy-client/libs/vorgang-shared/src/index.ts +++ b/goofy-client/libs/vorgang-shared/src/index.ts @@ -1,7 +1,3 @@ -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'; 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 df4157e48c0d0a928a9bfa3bc545df7749e85f61..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,6 +1,49 @@ -import { createAction, props } from '@ngrx/store'; +import { ApiRootResource } from '@goofy-client/api-root-shared'; +import { ApiError } from '@goofy-client/tech-shared'; +import { ActionCreator, createAction, props } from '@ngrx/store'; +import { TypedAction } from '@ngrx/store/src/models'; import { VorgangListResource } from '../vorgang.model'; -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<{ error: unknown }>()); +export interface VorgangActionCreator<T> extends ActionCreator<string, (props: T) => T & TypedAction<string>> { } +export interface TypedActionCreator extends ActionCreator<string, () => TypedAction<string>> { } + +export interface SearchVorgaengeByProps { + apiRoot: ApiRootResource, + searchString: string, + linkRel: string +} + +export interface StringBasedProps { + string: string +} + +export interface ApiRootAction { + apiRoot: ApiRootResource +} + +export interface ApiErrorAction { + apiError: ApiError +} + +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 2a7256dca0822b5d11f1f38749f28e9ab57b1a6a..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,26 +1,34 @@ import { TestBed } from '@angular/core/testing'; -import { ApiRootFacade, ApiRootResource } from '@goofy-client/api-root-shared'; -import { createStateResource } from '@goofy-client/tech-shared'; +import { ApiRootFacade, ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-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 { Action } from '@ngrx/store'; -import { provideMockStore } from '@ngrx/store/testing'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { NxModule } from '@nrwl/angular'; import { cold, hot } from 'jest-marbles'; 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 { 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 * as VorgangSelectors from './vorgang.selectors'; describe('VorgangEffects', () => { let actions: Observable<Action>; let effects: VorgangEffects; + let store: MockStore; const apiRootFacade = mock(ApiRootFacade); const vorgangRepository = mock(VorgangRepository); + const navigationFacade = mock(NavigationFacade); + + const vorgangList: VorgangListResource = createVorgangListResource(); beforeEach(() => { TestBed.configureTestingModule({ @@ -36,56 +44,229 @@ describe('VorgangEffects', () => { { provide: VorgangRepository, useValue: vorgangRepository + }, + { + provide: NavigationFacade, + useValue: navigationFacade } - ], + ] }); effects = TestBed.inject(VorgangEffects); + + store = TestBed.inject(MockStore); + store.overrideSelector(VorgangSelectors.vorgangList, createStateResource(vorgangList)); }); describe('loadVorgangList', () => { const vorgangList: VorgangListResource = createVorgangListResource(); const apiRoot: ApiRootResource = createApiRootResource(); + const action = VorgangActions.loadVorgangList({ apiRoot }); + + it('should call repository', () => { + actions = of(action); + + effects.loadVorgangList$.subscribe(); + + expect(vorgangRepository.loadVorgangList).toHaveBeenCalledWith(apiRoot); + }) + + it('should dispatch loadVorgangListSuccess action', () => { + vorgangRepository.loadVorgangList.mockReturnValue(of(vorgangList)); + + actions = hot('-a-|', { a: action }); + + const expected = hot('-a-|', { a: VorgangActions.loadVorgangListSuccess({ vorgangList }) }); + expect(effects.loadVorgangList$).toBeObservable(expected); + }) + + it('should dispatch loadVorgangListFailure action', () => { + const apiError: ApiError = createApiError() + const error = { error: { error: apiError } }; + const errorResponse = cold('-#', {}, error); + vorgangRepository.loadVorgangList = jest.fn(() => errorResponse); + + const expected = cold('--b', { b: VorgangActions.loadVorgangListFailure({ apiError }) }); + actions = hot('-a', { a: action }); + + expect(effects.loadVorgangList$).toBeObservable(expected); + }) + }) + + describe('searchVorgaengeBy', () => { + + const vorgangList: VorgangListResource = createVorgangListResource(); + const apiRoot: ApiRootResource = createApiRootResource(); + 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)); + + actions = hot('-a-|', { a: action }); + + const expected = hot('-a-|', { a: VorgangActions.searchVorgaengeBySuccess({ vorgangList }) }); + expect(effects.searchVorgaengeBy$).toBeObservable(expected); + }) + + it('should dispatch loadVorgangListFailure action', () => { + const apiError: ApiError = createApiError() + const error = { error: { error: apiError } }; + const errorResponse = cold('-#', {}, error); + vorgangRepository.searchVorgaengeBy = jest.fn(() => errorResponse); + + const expected = cold('--b', { b: VorgangActions.loadVorgangListFailure({ apiError }) }); + actions = hot('-a', { a: action }); + + expect(effects.searchVorgaengeBy$).toBeObservable(expected); + }) + }) + + describe('loadMyVorgaengeList', () => { + + const vorgangList: VorgangListResource = createVorgangListResource(); + const apiRoot: ApiRootResource = createApiRootResource(); + const action = VorgangActions.loadMyVorgaengeList({ apiRoot }); + + it('should call repository', () => { + actions = of(action); + + effects.loadMyVorgaengeList$.subscribe(); + + expect(vorgangRepository.loadMyVorgaengeList).toHaveBeenCalledWith(apiRoot); + }) + + it('should dispatch loadVorgangListSuccess action', () => { + vorgangRepository.loadMyVorgaengeList.mockReturnValue(of(vorgangList)); + + 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); + + const expected = cold('--b', { b: VorgangActions.loadVorgangListFailure({ apiError }) }); + actions = hot('-a', { a: action }); + + expect(effects.loadMyVorgaengeList$).toBeObservable(expected); + }) + }) + + describe('loadNextPage', () => { + + const action = VorgangActions.loadNextPage(); + + it('should select vorgangList from store', () => { + store.select = jest.fn(); + actions = of(action); + + effects.loadNextPage$.subscribe(); + + expect(store.select).toHaveBeenCalledWith(VorgangSelectors.vorgangList); + }) + + it('should call vorgang repository', () => { + actions = of(action); + + effects.loadNextPage$.subscribe(); + + expect(vorgangRepository.getNextVorgangListPage).toHaveBeenCalledWith(vorgangList); + }) + + it('should dispatch loadVorgangListSuccess action', () => { + vorgangRepository.getNextVorgangListPage.mockReturnValue(of(vorgangList)); + + actions = hot('-a-|', { a: action }); + + const expected = hot('-a-|', { a: VorgangActions.loadNextPageSuccess({ vorgangList }) }); + expect(effects.loadNextPage$).toBeObservable(expected); + }) + + it('should dispatch loadVorgangListFailure action', () => { + const apiError: ApiError = createApiError() + const error = { error: { error: apiError } }; + const errorResponse = cold('-#', {}, error); + vorgangRepository.getNextVorgangListPage = jest.fn(() => errorResponse); + + const expected = cold('--c', { c: VorgangActions.loadVorgangListFailure({ apiError }) }); + actions = hot('-a', { a: action }); + + expect(effects.loadNextPage$).toBeObservable(expected); + }) + }) + + describe('searchForPreview', () => { + + const vorgangList: VorgangListResource = createVorgangListResource(); + const apiRoot: ApiRootResource = createApiRootResource(); + + const searchString: string = 'searchThisForMe'; + const action = VorgangActions.searchForPreview({ string: searchString }); beforeEach(() => { apiRootFacade.getApiRoot.mockReturnValue(of(createStateResource(apiRoot))); + navigationFacade.getCurrentRouteData.mockReturnValue(of(createRouteData())); }) - it('should call api root facade', () => { - actions = of(VorgangActions.loadVorgangList()); + it('should call apiRootFacade', () => { + actions = of(action); - effects.loadVorgangList.subscribe(); + effects.searchForPreview$.subscribe(); expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); }) + it('should call navigationFacade', () => { + actions = of(action); + + effects.searchForPreview$.subscribe(); + + expect(navigationFacade.getCurrentRouteData).toHaveBeenCalled(); + }) + it('should call vorgang repository', () => { - actions = of(VorgangActions.loadVorgangList()); + actions = of(action); - effects.loadVorgangList.subscribe(); + effects.searchForPreview$.subscribe(); - expect(vorgangRepository.loadVorgangList).toHaveBeenCalledWith(apiRoot); + expect(vorgangRepository.searchVorgaengeBy).toHaveBeenCalledWith(apiRoot, searchString, ApiRootLinkRel.SEARCH, 7); }) - it('should dispatch loadVorgangListSuccess action', () => { - vorgangRepository.loadVorgangList.mockReturnValue(of(vorgangList)); + it('should dispatch searchForPreviewSuccess action', () => { + vorgangRepository.searchVorgaengeBy.mockReturnValue(of(vorgangList)); - actions = hot('-a-|', { a: VorgangActions.loadVorgangList() }); + actions = hot('-a-|', { a: action }); - const expected = hot('-a-|', { a: VorgangActions.loadVorgangListSuccess({ loadedResource: vorgangList }) }); - expect(effects.loadVorgangList).toBeObservable(expected); + const expected = hot('-a-|', { a: VorgangActions.searchForPreviewSuccess({ vorgangList }) }); + expect(effects.searchForPreview$).toBeObservable(expected); }) - it('should dispatch loadVorgangListFailure action', () => { - const error = 'an error occured'; + it('should dispatch searchForPreviewFailure action', () => { + const apiError: ApiError = createApiError() + const error = { error: { error: apiError } }; const errorResponse = cold('-#', {}, error); - vorgangRepository.loadVorgangList = jest.fn(() => errorResponse); + vorgangRepository.searchVorgaengeBy = jest.fn(() => errorResponse); - const expected = cold('--c', { c: VorgangActions.loadVorgangListFailure({ error }) }); - actions = hot('-a', { a: VorgangActions.loadVorgangList() }); + const expected = cold('--c', { c: VorgangActions.searchForPreviewFailure({ apiError }) }); + actions = hot('-a', { a: action }); - expect(effects.loadVorgangList).toBeObservable(expected); + expect(effects.searchForPreview$).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 02a05e9aad3ad1b7d0ded1b21614281585d9c742..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,26 +1,81 @@ import { Injectable } from '@angular/core'; 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 { Store } from '@ngrx/store'; import { of } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; +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() export class VorgangEffects { - constructor(private readonly actions$: Actions, private repository: VorgangRepository, private apiRootFacade: ApiRootFacade) { } + static readonly SEARCH_PREVIEW_LIST_LIMIT: number = 7; + static readonly SEARCH_QUERY_PARAM: string = 'search'; + static readonly MY_VORGAENGE_URI_SEGMENT: string = 'myVorgaenge'; - loadVorgangList = createEffect(() => + constructor(private readonly actions$: Actions, private store: Store, private repository: VorgangRepository, private apiRootFacade: ApiRootFacade, private navigationFacade: NavigationFacade) { } + + loadVorgangList$ = createEffect(() => this.actions$.pipe( ofType(VorgangActions.loadVorgangList), - concatLatestFrom(() => this.apiRootFacade.getApiRoot()), - switchMap(([, apiRoot]) => { - return this.repository.loadVorgangList(apiRoot.resource).pipe( - map(listResource => VorgangActions.loadVorgangListSuccess({ loadedResource: listResource })), - catchError(error => of(VorgangActions.loadVorgangListFailure({ 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) }))) + )) + ) + ) + + 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]) => this.repository.getNextVorgangListPage(vorgangList.resource).pipe( + map(loadedVorgangList => VorgangActions.loadNextPageSuccess({ vorgangList: loadedVorgangList })), + catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) }))) + )) + ) + ) + + searchForPreview$ = createEffect(() => + this.actions$.pipe( + ofType(VorgangActions.searchForPreview), + concatLatestFrom(() => [this.apiRootFacade.getApiRoot(), this.navigationFacade.getCurrentRouteData()]), + 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): ApiError { + return error.error.error; + } } \ 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 4da2900b2e136511dae5413ed56fb5b6f9e68486..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,9 +1,11 @@ -import { createEmptyStateResource, createStateResource, 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 { createVorgangListResource } from 'libs/vorgang-shared/test/vorgang'; +import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; +import { createVorgangListResource, createVorgangResources } from 'libs/vorgang-shared/test/vorgang'; import { Subject } from 'rxjs'; -import { VorgangListResource } from '../vorgang.model'; +import { SearchInfo, VorgangListResource, VorgangResource } from '../vorgang.model'; import * as VorgangActions from './vorgang.actions'; import { VorgangFacade } from './vorgang.facade'; @@ -11,7 +13,7 @@ describe('VorgangFacade', () => { let facade: VorgangFacade; let store: Mock<Store>; - let selectionSubject: Subject<StateResource<VorgangListResource>>; + let selectionSubject: Subject<any>; beforeEach(() => { store = mock(Store); @@ -31,25 +33,118 @@ describe('VorgangFacade', () => { describe('getVorgangList', () => { it('should return selected value', (done) => { - const vorgangListStateResource: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + const vorgaengeStateResource: StateResource<VorgangResource[]> = createStateResource(createVorgangResources()); - facade.getVorgangList().subscribe(selectorValue => { - expect(selectorValue).toBe(vorgangListStateResource); + facade.getVorgangList().subscribe(vorgaenge => { + expect(vorgaenge).toBe(vorgaengeStateResource); done(); }); - selectionSubject.next(vorgangListStateResource); + selectionSubject.next(vorgaengeStateResource); }); + }) + + describe('loadVorgangList', () => { + + it('should dispatch "loadVorgangList" action', () => { + const apiRoot: ApiRootResource = createApiRootResource(); + + facade.loadVorgangList(apiRoot); + + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.loadVorgangList({ apiRoot })); + }); + }) + + describe('searchVorgaengeBy', () => { + + const apiRoot: ApiRootResource = createApiRootResource(); + const searchString: string = 'search like me'; + const linkRel: string = 'LinkRelationName'; + + it('should dispatch "searchVorgaengeBy" action', () => { + facade.searchVorgaengeBy(apiRoot, searchString, linkRel); + + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.searchVorgaengeBy({ apiRoot, searchString, linkRel })); + }); + }) + + describe('loadMyVorgaengeList', () => { - it('should dispatch action if not loaded', (done) => { - facade.getVorgangList().subscribe(historieList => { - expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.loadVorgangList()); - expect(historieList.loading).toBe(true); + it('should dispatch "loadMyVorgaengeList" action', () => { + const apiRoot: ApiRootResource = createApiRootResource(); + + facade.loadMyVorgaengeList(apiRoot); + + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.loadMyVorgaengeList({ apiRoot })); + }); + }) + + describe('getVorgaenge', () => { + + it('should return selected value', (done) => { + const vorgaengeStateResource: StateResource<VorgangResource[]> = createStateResource(createVorgangResources()); + + facade.getVorgaenge().subscribe(vorgaenge => { + expect(vorgaenge).toBe(vorgaengeStateResource); done(); }); - selectionSubject.next(createEmptyStateResource()); - selectionSubject.next(createEmptyStateResource(true)); + selectionSubject.next(vorgaengeStateResource); + }); + }) + + describe('getSearchInfo', () => { + + it('should return selected value', (done) => { + const searchInfoStateValue: StateResource<SearchInfo> = createStateResource({ changedAfterSearchDone: false, searchString: EMPTY_STRING }); + + facade.getSearchInfo().subscribe(searchInfo => { + expect(searchInfo).toBe(searchInfoStateValue); + done(); + }); + + selectionSubject.next(searchInfoStateValue); + }); + }) + + describe('loadNextPage', () => { + + it('should dispatch "loadNextPage" action', () => { + facade.loadNextPage(); + + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.loadNextPage()); + }); + }) + + describe('searchForPreview', () => { + + it('should dispatch "searchForPreview" action', () => { + facade.searchForPreview('searchThisForMe'); + + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.searchForPreview({ string: 'searchThisForMe' })); + }); + }) + + describe('getSearchPreviewList', () => { + + it('should return selected value', (done) => { + const saerchPreviewListStateResource: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + + facade.getSearchPreviewList().subscribe(searchPreviewList => { + expect(searchPreviewList).toBe(saerchPreviewListStateResource); + done(); + }); + + selectionSubject.next(saerchPreviewListStateResource); + }); + }) + + describe('clearSearchPreviewList', () => { + + it('should dispatch "clearSearchPreviewList" action', () => { + facade.clearSearchPreviewList(); + + expect(store.dispatch).toHaveBeenCalledWith(VorgangActions.clearSearchPreviewList()); }); }) }) \ No newline at end of file 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 befe75dff18989b69c5f09ae26611bbecc738d24..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,9 +1,9 @@ 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 { VorgangListResource } from '../vorgang.model'; +import { SearchInfo, VorgangListResource, VorgangResource } from '../vorgang.model'; import * as VorgangActions from './vorgang.actions'; import * as VorgangSelectors from './vorgang.selectors'; @@ -13,7 +13,42 @@ 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[]> { + return this.store.select(VorgangSelectors.vorgaenge); + } + + public getSearchInfo(): Observable<SearchInfo> { + return this.store.select(VorgangSelectors.searchInfo); + } + + public loadNextPage(): void { + this.store.dispatch(VorgangActions.loadNextPage()); + } + + public searchForPreview(searchString: string): void { + this.store.dispatch(VorgangActions.searchForPreview({ string: searchString })); + } + + public getSearchPreviewList(): Observable<StateResource<VorgangListResource>> { + return this.store.select(VorgangSelectors.searchPreviewList); + } + + public clearSearchPreviewList(): void { + this.store.dispatch(VorgangActions.clearSearchPreviewList()); } } \ No newline at end of file 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 2f38ada8c3fb3f6f87c075614970e5924c57834a..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,15 +1,25 @@ -import { createStateResource } from '@goofy-client/tech-shared'; +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 } from 'libs/vorgang-shared/test/vorgang'; -import { VorgangListResource } from '../vorgang.model'; +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', () => { describe('unknown action', () => { - it('should return the previous state', () => { + it('should return current state', () => { const action = {} as Action; const result = reducer(initialState, action); @@ -18,38 +28,311 @@ describe('Vorgang Reducer', () => { }) }) - describe('on loadVorgangList action', () => { + describe('loadVorgangList', () => { - it('should set loading to true', () => { - const action = VorgangActions.loadVorgangList(); + describe('on "loadVorgangList" action', () => { - const state: VorgangState = reducer(initialState, action); + it('should set loading to true', () => { + const action = VorgangActions.loadVorgangList({ apiRoot: createApiRootResource() }); - expect(state.vorgangList.loading).toBeTruthy(); + 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); + + expect(state.vorgangList.loading).toBeTruthy(); + }) + }) + + describe('on "loadVorgangListSuccess" action', () => { + + const vorgaenge: VorgangResource[] = createVorgangResources(); + 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(vorgangList)); + }) + + it('should set vorgaenge', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgaenge.length).toBe(10); + expect(state.vorgaenge).toStrictEqual(vorgaenge); + }) + + it('should clear searchPreviewList', () => { + const state: VorgangState = reducer({ ...initialState, searchPreviewList: createStateResource(createVorgangListResource()) }, action); + + expect(state.searchPreviewList).toEqual(createEmptyStateResource()); + }) + }) + + describe('on "loadVorgangListFailure" action', () => { + + it('should set apiError', () => { + const apiError: ApiError = createApiError(); + const action = VorgangActions.loadVorgangListFailure({ apiError }); + + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList.error).toStrictEqual(apiError); + }) + + it('should clear searchPreviewList', () => { + const apiError: ApiError = createApiError(); + const action = VorgangActions.loadVorgangListFailure({ apiError }); + + const state: VorgangState = reducer(initialState, action); + + expect(state.searchPreviewList).toEqual(createEmptyStateResource()); + }) }) }) - describe('on loadVorgangListSuccess action', () => { + describe('loadNextPage', () => { + + describe('on "loadNextPage" action', () => { + + it('should set vorgangList reload to true', () => { + const action = VorgangActions.loadNextPage(); + + const state: VorgangState = reducer(initialState, action); + + expect(state.vorgangList.reload).toBeTruthy(); + }) + }) + + describe('on "loadNextPageSuccess" action', () => { + + const vorgangList: VorgangListResource = createVorgangListResource(); + + it('should set vorgangList', () => { + const action = VorgangActions.loadNextPageSuccess({ vorgangList }); - it('should set loaded resource into state', () => { - const listResource: VorgangListResource = createVorgangListResource(); - const action = VorgangActions.loadVorgangListSuccess({ loadedResource: listResource }); + const state: VorgangState = reducer(initialState, action); - const state: VorgangState = reducer(initialState, action); + expect(state.vorgangList).toStrictEqual(createStateResource(vorgangList)); + }) - expect(state.vorgangList).toEqual(createStateResource(listResource)); + 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 loadVorgangListFailure action', () => { + describe('searchVorgaengeBy', () => { + + describe('on "searchVorgaengeBy" action', () => { - it('should set error into state', () => { - const error = <Error>{};//TODO durch richtigen Error ersetzt - const action = VorgangActions.loadVorgangListFailure({ error }); + const apiRoot: ApiRootResource = createApiRootResource(); + const searchString: string = 'search like me'; + const linkRel: string = 'LinkRelationName'; - const state: VorgangState = reducer(initialState, action); + const action = VorgangActions.searchVorgaengeBy({ apiRoot, searchString, linkRel }); - expect(state.vorgangList.error).toBe(error); + 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); + + expect(state.vorgaenge).toBe(EMPTY_ARRAY); + }) + + it('should clear searchPreviewList', () => { + const state: VorgangState = reducer(initialState, action); + + 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()); + }) + }) + }) + + describe('searchForPreview', () => { + + describe('on "searchForPreview" action', () => { + + const searchString: string = 'searchThisForMe'; + const action = VorgangActions.searchForPreview({ string: searchString }); + + it('should set loading to true', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.searchPreviewList.loading).toBeTruthy(); + }) + }) + + describe('on "searchForPreviewSuccess" action', () => { + + 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).toStrictEqual(vorgangList); + }) + }) + + describe('on "searchForPreviewFailure" action', () => { + + const apiError: ApiError = createApiError(); + const action = VorgangActions.searchForPreviewFailure({ apiError }); + + it('should set error', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.searchPreviewList.error).toStrictEqual(apiError); + }) + }) + + describe('on "clearSearchPreviewList" action', () => { + + const action = VorgangActions.clearSearchPreviewList(); + + it('should clear state resource', () => { + const state: VorgangState = reducer(initialState, action); + + expect(state.searchPreviewList).toEqual(createEmptyStateResource()); + }) + }) + }) + + 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 51f6015ebfc31dc676dc5f0dc193826c839ec29b..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,38 +1,148 @@ -import { createEmptyStateResource, createErrorStateResource, createStateResource, 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 { VorgangListResource } from '../vorgang.model'; +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'; -export interface VorgangState { - vorgangList: StateResource<VorgangListResource>; -} - export interface VorgangPartialState { readonly [VORGANG_FEATURE_KEY]: VorgangState; } +export interface VorgangState { + vorgangList: StateResource<VorgangListResource>; + vorgaenge: VorgangResource[]; + searchInfo: SearchInfo; + searchPreviewList: StateResource<VorgangListResource>; +} + export const initialState: VorgangState = { - vorgangList: createEmptyStateResource() + vorgangList: createEmptyStateResource(), + vorgaenge: EMPTY_ARRAY, + 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.loadMyVorgaengeList, (state: VorgangState) => ({ ...state, vorgangList: { ...state.vorgangList, loading: true } })), - on(VorgangActions.loadVorgangListSuccess, (state, { loadedResource }) => ({ + 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.loadNextPage, (state: VorgangState): VorgangState => ({ + ...state, + vorgangList: { ...state.vorgangList, reload: true }, + })), + on(VorgangActions.loadNextPageSuccess, (state, action: VorgangListAction): VorgangState => ({ + ...state, + vorgangList: createStateResource<VorgangListResource>(action.vorgangList), + vorgaenge: [...state.vorgaenge].concat(getVorgaengeFromList(action.vorgangList)), + })), + + on(VorgangActions.searchVorgaengeBy, (state: VorgangState): VorgangState => ({ + ...state, + 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: VorgangState): VorgangState => ({ + ...state, + searchPreviewList: { ...state.searchPreviewList, loading: true } + })), + on(VorgangActions.searchForPreviewSuccess, (state: VorgangState, action: VorgangListAction): VorgangState => ({ ...state, - vorgangList: createStateResource<VorgangListResource>(loadedResource) + searchPreviewList: createStateResource<VorgangListResource>(action.vorgangList) })), - on(VorgangActions.loadVorgangListFailure, (state, { error }) => ({ + on(VorgangActions.searchForPreviewFailure, (state: VorgangState, action: ApiErrorAction): VorgangState => ({ ...state, - vorgangList: createErrorStateResource<any>(<any>error) - })) + searchPreviewList: createErrorStateResource(action.apiError) + })), + on(VorgangActions.clearSearchPreviewList, (state: VorgangState): VorgangState => ({ + ...state, + 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/+state/vorgang.selectors.spec.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.spec.ts index e788ef6597a57ec5bca684288f85894977d832c3..c19b3657cf98ef6ad4aac57a288bd7a88e2554ea 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.spec.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.spec.ts @@ -1,6 +1,6 @@ import { createStateResource, StateResource } from '@goofy-client/tech-shared'; -import { createVorgangListResource } from 'libs/vorgang-shared/test/vorgang'; -import { VorgangListResource } from '../vorgang.model'; +import { createVorgangListResource, createVorgangResources } from 'libs/vorgang-shared/test/vorgang'; +import { SearchInfo, VorgangListResource, VorgangResource } from '../vorgang.model'; import { initialState, VorgangPartialState } from './vorgang.reducer'; import * as VorgangSelectors from './vorgang.selectors'; @@ -9,12 +9,18 @@ describe('Vorgang Selectors', () => { let state: VorgangPartialState; const vorgangList: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + const vorgaenge: VorgangResource[] = createVorgangResources(); + const searchPreviewList: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + const searchInfo: SearchInfo = { changedAfterSearchDone: false, searchString: 'searchThisForMe' }; beforeEach(() => { state = { VorgangState: { ...initialState, - vorgangList + vorgangList, + vorgaenge, + searchPreviewList, + searchInfo } }; }); @@ -22,4 +28,16 @@ describe('Vorgang Selectors', () => { it('should return vorgangList', () => { expect(VorgangSelectors.vorgangList.projector(state.VorgangState)).toBe(vorgangList); }) -}); + + it('should return vorgaenge', () => { + expect(VorgangSelectors.vorgaenge.projector(state.VorgangState)).toBe(vorgaenge); + }) + + it('should return searchPreviewList', () => { + expect(VorgangSelectors.searchPreviewList.projector(state.VorgangState)).toBe(searchPreviewList); + }) + + it('should return searchInfo', () => { + expect(VorgangSelectors.searchInfo.projector(state.VorgangState)).toBe(searchInfo); + }) +}); \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.ts b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.ts index ad7afacbcc50f69f08580f242f78b1ffbb0107bd..df4eacbd5cbc44377cf43195f3720025696df083 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.ts @@ -1,7 +1,12 @@ -import { createFeatureSelector, createSelector } from '@ngrx/store'; +import { StateResource } from '@goofy-client/tech-shared'; +import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; +import { SearchInfo, VorgangListResource, VorgangResource } from '../vorgang.model'; import { VorgangState, VORGANG_FEATURE_KEY } from './vorgang.reducer'; -// Lookup the 'Vorgang' feature state managed by NgRx -export const getVorgangState = createFeatureSelector<VorgangState>(VORGANG_FEATURE_KEY); +export const getVorgangState: MemoizedSelector<object, VorgangState> = createFeatureSelector<VorgangState>(VORGANG_FEATURE_KEY); -export const vorgangList = createSelector(getVorgangState, (state: VorgangState) => state.vorgangList) \ No newline at end of file +export const vorgangList: MemoizedSelector<VorgangState, StateResource<VorgangListResource>> = createSelector(getVorgangState, (state: VorgangState) => state.vorgangList); +export const vorgaenge: MemoizedSelector<VorgangState, VorgangResource[]> = createSelector(getVorgangState, (state: VorgangState) => state.vorgaenge); + +export const searchPreviewList: MemoizedSelector<VorgangState, StateResource<VorgangListResource>> = createSelector(getVorgangState, (state: VorgangState) => state.searchPreviewList); +export const searchInfo: MemoizedSelector<VorgangState, SearchInfo> = createSelector(getVorgangState, (state: VorgangState) => state.searchInfo); \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/vorgang-command.service.spec.ts b/goofy-client/libs/vorgang-shared/src/lib/vorgang-command.service.spec.ts index 9058baff226aab74b69d12d2319de0ccdc47ab85..1cfa60dfb055425a0a295765352c5499406911b5 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/vorgang-command.service.spec.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-command.service.spec.ts @@ -9,7 +9,6 @@ import { createCommandResource } from 'libs/command-shared/test/command'; import { createVorgangWithEingangResource } from 'libs/vorgang-shared/test/vorgang'; import { of } from 'rxjs'; import { VorgangCommandService } from './vorgang-command.service'; -import { VorgangListService } from './vorgang-list.service'; import { VorgangWithEingangLinkRel } from './vorgang.linkrel'; import { VorgangMessages } from './vorgang.messages'; import { VorgangOrder, VorgangWithEingangResource } from './vorgang.model'; @@ -21,20 +20,18 @@ describe('VorgangCommandService', () => { let navigationService: Mock<NavigationService>; let commandService: Mock<CommandService>; let snackBarService: Mock<SnackBarService>; - let vorgangListService: Mock<VorgangListService>; let vorgangService: Mock<VorgangService>; beforeEach(() => { navigationService = { ...mock(NavigationService) }; commandService = mock(CommandService); snackBarService = mock(SnackBarService); - vorgangListService = mock(VorgangListService); vorgangService = mock(VorgangService); navigationService.urlChanged = jest.fn(); navigationService.urlChanged.mockReturnValue(of({})); - service = new VorgangCommandService(useFromMock(navigationService), useFromMock(commandService), useFromMock(snackBarService), useFromMock(vorgangListService), useFromMock(vorgangService)); + service = new VorgangCommandService(useFromMock(navigationService), useFromMock(commandService), useFromMock(snackBarService), useFromMock(vorgangService)); }) describe('onNavigation', () => { @@ -165,17 +162,10 @@ describe('VorgangCommandService', () => { expect(vorgangService.reloadVorgang).toHaveBeenCalled(); }) - it('should reload vorgang list if command is done', () => { - service.proceedWithRevokeCommand(createStateResource(createCommandResource([CommandLinkRel.EFFECTED_RESOURCE]))); - - expect(vorgangListService.reloadVorgangList).toHaveBeenCalled(); - }) - it('should not proceed if command is pending', () => { service.proceedWithRevokeCommand(createStateResource(createCommandResource())); expect(vorgangService.reloadVorgang).not.toHaveBeenCalled(); - expect(vorgangListService.reloadVorgangList).not.toHaveBeenCalled(); }); }) }) \ No newline at end of file diff --git a/goofy-client/libs/vorgang-shared/src/lib/vorgang-command.service.ts b/goofy-client/libs/vorgang-shared/src/lib/vorgang-command.service.ts index 673443cdea57e09b74743190f363f12d2d5dbaa3..bc9311632040401afb1744687ee943cec016a464 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/vorgang-command.service.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-command.service.ts @@ -4,7 +4,6 @@ import { CommandResource, CommandService, CreateCommand, isDone } from '@goofy-c import { createEmptyStateResource, isNotUndefined, NavigationService, StateResource } from '@goofy-client/tech-shared'; import { SnackBarService } from '@goofy-client/ui'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { VorgangListService } from './vorgang-list.service'; import { VorgangWithEingangLinkRel } from './vorgang.linkrel'; import { VorgangMessages } from './vorgang.messages'; import { VorgangOrder } from './vorgang.model'; @@ -35,7 +34,6 @@ export class VorgangCommandService { private navigationService: NavigationService, private commandService: CommandService, private snackBarService: SnackBarService, - private vorgangListService: VorgangListService, private vorgangService: VorgangService ) { this.listenForLeavingVorgang(); @@ -140,7 +138,6 @@ export class VorgangCommandService { if (isDone(commandStateResource.resource)) { this.revokeCommand$.next(commandStateResource); this.vorgangService.reloadVorgang(commandStateResource.resource); - this.vorgangListService.reloadVorgangList(); } } } 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 index 9a49e75422b3dafe10659bfc45c7b38e0ed65c6a..db668cc5f425be192bb5d0138f5e7a86d4cc72e5 100644 --- 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 @@ -1,412 +1,281 @@ import { ApiRootFacade, ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-shared'; -import { createEmptyStateResource, createStateResource, EMPTY_STRING, NavigationService } from '@goofy-client/tech-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 { getEmbeddedResource } from '@ngxp/rest'; -import { cold } from 'jest-marbles'; +import { cold, hot } from 'jest-marbles'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; -import { createVorgangListResource, createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; -import { of } from 'rxjs'; -import { SearchInfo } from '..'; +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 { VorgangListLinkRel } from './vorgang.linkrel'; -import { VorgangListResource, VorgangResource } from './vorgang.model'; -import { VorgangRepository } from './vorgang.repository'; +import { SearchInfo, VorgangListResource } from './vorgang.model'; describe('VorgangListService', () => { let service: VorgangListService; - let navigationService: Mock<NavigationService>; - let repository: Mock<VorgangRepository>; + let vorgangFacade: Mock<VorgangFacade>; let apiRootFacade: Mock<ApiRootFacade>; - - const vorgangListResource: VorgangListResource = createVorgangListResource(); - const vorgaengeOfVorgangList: VorgangResource[] = getEmbeddedResource(vorgangListResource, VorgangListLinkRel.VORGANG_HEADER_LIST); + let navigationFacade: Mock<NavigationFacade>; beforeEach(() => { - repository = mock(VorgangRepository); + vorgangFacade = mock(VorgangFacade); apiRootFacade = mock(ApiRootFacade); - navigationService = { ...mock(NavigationService) }; - navigationService.urlChanged = jest.fn(); - navigationService.urlChanged.mockReturnValue(of({})); + navigationFacade = mock(NavigationFacade); - service = new VorgangListService(useFromMock(apiRootFacade), useFromMock(repository), useFromMock(navigationService)); + service = new VorgangListService(useFromMock(vorgangFacade), useFromMock(apiRootFacade), useFromMock(navigationFacade)); }) describe('getVorgangList', () => { - const apiRootResource: ApiRootResource = createApiRootResource(); - - beforeEach(() => { - apiRootFacade.getApiRoot.mockReturnValue(of(createStateResource(apiRootResource))); - }) - - it('should call repository if not loaded', () => { - service.vorgangList$.next(createEmptyStateResource()); - - service.getVorgangList(); - - expect(repository.loadVorgangList).toHaveBeenCalledWith(apiRootResource); - }) - it('should not call repository if already loading', () => { - service.vorgangList$.next(createEmptyStateResource(true)); - - service.getVorgangList(); - - expect(repository.loadVorgangList).not.toHaveBeenCalled(); - }) - - describe('set loading', () => { - - beforeEach(() => { - repository.loadVorgangList.mockReturnValue(of(null)); - }) + describe('load required data', () => { - it('should set loading flag', () => { + it('should get vorgang list by facade', () => { service.getVorgangList(); - expect(service.vorgangList$.value.loading).toBe(true); - }); - }) - - describe('check vorgangList after load ', () => { - - beforeEach(() => { - repository.loadVorgangList.mockReturnValue(of(vorgangListResource)); + expect(vorgangFacade.getVorgangList).toHaveBeenCalled(); }) - it('should set loaded', () => { + it('should get apiroot by service', () => { service.getVorgangList(); - expect(service.vorgangList$.value.loaded).toBe(true); - }); - - it('should set resource', () => { - service.getVorgangList(); - - expect(service.vorgangList$.value.resource).toBe(vorgangListResource); - }); - - it('should unset loading', () => { - service.getVorgangList(); - - expect(service.vorgangList$.value.loading).toBe(false); - }); - }) - - describe('check vorgaenge after load', () => { - - beforeEach(() => { - repository.loadVorgangList.mockReturnValue(of(vorgangListResource)); + expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); }) - it('should set vorgaenge to loaded vorgaenge', () => { + it('should get current route data by navigation facade', () => { service.getVorgangList(); - expect(service.vorgaenge$.value).toHaveLength(vorgaengeOfVorgangList.length); - expect(service.vorgaenge$.value).toBe(vorgaengeOfVorgangList); + expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); }) }) - describe('check hasNextPage after load', () => { + describe('on loaded resource', () => { - it('should be true if link exists', () => { - repository.loadVorgangList.mockReturnValue(of(createVorgangListResource([VorgangListLinkRel.NEXT]))); + const vorgangListStateResource: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + const apiRootStateResource: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); + const currentRouteData: StateResource<RouteData> = createStateResource(createRouteData()); - service.getVorgangList(); + beforeEach(() => { + service.loadVorgangList = jest.fn(); - expect(service.hasNextPage()).toBeObservable(cold('a', { a: true })); + vorgangFacade.getVorgangList.mockReturnValue(hot('-a', { a: vorgangListStateResource })); + apiRootFacade.getApiRoot.mockReturnValue(hot('-a', { a: apiRootStateResource })); + navigationFacade.getCurrentRouteData.mockReturnValue(hot('-a', { a: currentRouteData })); }) - it('should be false if link not exists', () => { - repository.loadVorgangList.mockReturnValue(of(createVorgangListResource())); - - service.getVorgangList(); + it('should return loaded resource', () => { + const vorgangList = service.getVorgangList(); - expect(service.hasNextPage()).toBeObservable(cold('a', { a: false })); + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: vorgangListStateResource })); }) - }) - describe('load vorgangList with empty ApiRoot resource', () => { - - beforeEach(() => { - apiRootFacade.getApiRoot.mockReturnValue(of(createEmptyStateResource())); - }) - - it('should not call repository loadVorgangList', () => { + it('should not load vorgangList', () => { service.getVorgangList(); - expect(repository.loadVorgangList).not.toHaveBeenCalled(); + expect(service.loadVorgangList).not.toHaveBeenCalled(); }) }) - }) - - describe('load next page', () => { - const vorgangListResource: VorgangListResource = createVorgangListResource(); - - beforeEach(() => { - repository.getNextVorgangListPage.mockReturnValue(of(vorgangListResource)); - }) - it('should not call repository while vorgangList is loading', () => { - service.vorgangList$.next(createEmptyStateResource(true)); + describe('on reload required', () => { - service.loadNextPage(); - - expect(repository.getNextVorgangListPage).not.toHaveBeenCalled(); - }) - - it('should call repository', () => { - service.vorgangList$.next(createStateResource(vorgangListResource)); - - service.loadNextPage(); - - expect(repository.getNextVorgangListPage).toHaveBeenCalledWith(vorgangListResource); - }) - - describe('set loading', () => { + 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(() => { - repository.getNextVorgangListPage.mockReturnValue(of(null)); - }) + service.loadVorgangList = jest.fn(); - it('should set loading flag', () => { - service.loadNextPage(); - - expect(service.vorgangList$.value.loading).toBe(true); - }); - }) - - describe('check vorgangList after load', () => { - - beforeEach(() => { - repository.getNextVorgangListPage.mockReturnValue(of(vorgangListResource)); + vorgangFacade.getVorgangList.mockReturnValue(hot('-a', { a: vorgangListStateResource })); + apiRootFacade.getApiRoot.mockReturnValue(hot('-a', { a: apiRootStateResource })); + navigationFacade.getCurrentRouteData.mockReturnValue(hot('-a', { a: currentRouteData })); }) - it('should set loaded', () => { - service.loadNextPage(); + it('should return value on loaded resource', () => { + const vorgangList = service.getVorgangList(); - expect(service.vorgangList$.value.loaded).toBe(true); - }); - - it('should set resource', () => { - service.loadNextPage(); - - expect(service.vorgangList$.value.resource).toBe(vorgangListResource); - }); - - it('should unset loading', () => { - service.loadNextPage(); - - expect(service.vorgangList$.value.loading).toBe(false); - }); - }) - - describe('check hasNextPage after load', () => { - - it('should be true if link exists', () => { - repository.getNextVorgangListPage.mockReturnValue(of(createVorgangListResource([VorgangListLinkRel.NEXT]))); - - service.loadNextPage(); - - expect(service.hasNextPage$.value).toBe(true); + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: { ...vorgangListStateResource, reload: true } })); }) - it('should be false if link not exists', () => { - repository.getNextVorgangListPage.mockReturnValue(of(createVorgangListResource())); + it.skip('FIXME: should load vorgangList', () => { + vorgangFacade.getVorgangList.mockReturnValue(hot('-ab', { a: { ...vorgangListStateResource, loading: true, reload: false }, b: { ...vorgangListStateResource, loading: true, reload: false } })); - service.loadNextPage(); + service.getVorgangList(); - expect(service.hasNextPage$.value).toBe(false); + expect(service.loadVorgangList).toHaveBeenCalled(); }) }) - }); + }) - describe('onNavigation', () => { + describe('loadVorgangList', () => { - describe('to myVorgaenge', () => { + const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.SEARCH]); + const routeData: RouteData = createRouteData(); - it('should set vorgang list to reload', () => { - service.setVorgangListToReload = jest.fn(); - navigationService.isMyVorgaengeNavigation.mockReturnValue(true); + describe('on existing search string', () => { + + const searchString: string = 'search like me'; - service.onNavigation({}); + it('should call facade searchVorgaengeBy', () => { + service.loadVorgangList(apiRootResource, { ...routeData, queryParameter: { ['search']: searchString }, urlSegments: [<any>{ path: VorgangEffects.SEARCH_QUERY_PARAM }, { path: searchString }] }); - expect(service.setVorgangListToReload).toHaveBeenCalled(); + expect(vorgangFacade.searchVorgaengeBy).toHaveBeenCalledWith(apiRootResource, searchString, ApiRootLinkRel.SEARCH); }) }) - describe('to vorgang list page', () => { - - beforeEach(() => { - service.setSearchInfo = jest.fn(); - }) + describe('on "myVorgaenge"', () => { - it('should clear saerchInfo', () => { - service.onNavigation({}); + it('should call facade loadMyVorgaengeList', () => { + service.loadVorgangList(apiRootResource, { ...routeData, urlSegments: [<any>{ path: 'myVorgaenge' }] }); - expect(service.setSearchInfo).toHaveBeenCalledWith(EMPTY_STRING, false); + expect(vorgangFacade.loadMyVorgaengeList).toHaveBeenCalledWith(apiRootResource); }) - }); + }) - describe('search', () => { + describe('on vorganglist loadVorgangList', () => { - const searchString: string = 'X'; + it('should call facade loadMyVorgaengeList', () => { + service.loadVorgangList(apiRootResource, routeData); - beforeEach(() => { - service.setVorgangListToReload = jest.fn(); - service.isVorgangListLoaded = jest.fn(); - (<any>service).isVorgangListLoaded.mockReturnValue(true); - service.setSearchInfo = jest.fn(); - service.getVorgangList = jest.fn(); - service.clearVorgangSearchPreviewList = jest.fn(); + expect(vorgangFacade.loadVorgangList).toHaveBeenCalledWith(apiRootResource); }) + }) + }) - it('should clear vorgang list', () => { - service.onNavigation({ search: searchString }); - - expect(service.setVorgangListToReload).toHaveBeenCalled(); - }) + describe('clearSearchPreviewList', () => { - it('should load vorgang list', () => { - service.onNavigation({ search: searchString }); + it('should call facade', () => { + service.clearSearchPreviewList(); - expect(service.getVorgangList).toHaveBeenCalled(); - }) + expect(vorgangFacade.clearSearchPreviewList).toHaveBeenCalled(); + }) + }) - it('should set search sting', () => { - navigationService.getDecodedUriParam.mockReturnValue(searchString); + describe('getVorgaenge', () => { - service.onNavigation({ search: searchString }); + it('should call facade', () => { + service.getVorgaenge(); - expect(service.setSearchInfo).toHaveBeenCalledWith(searchString, true); - }); + expect(vorgangFacade.getVorgaenge).toHaveBeenCalled(); + }) + }) - it('should clear search preview list', () => { - navigationService.getDecodedUriParam.mockReturnValue(searchString); + describe('loadNextPage', () => { - service.onNavigation({ search: searchString }); + it('should call facade', () => { + service.loadNextPage(); - expect(service.clearVorgangSearchPreviewList).toHaveBeenCalled(); - }) - }); + expect(vorgangFacade.loadNextPage).toHaveBeenCalled(); + }) + }) - describe('to vorgang detail page', () => { + describe('getSearchInfo', () => { - beforeEach(() => { - service.setSearchInfo = jest.fn(); - }) + const searchInfo: SearchInfo = <any>{}; - it('should clear searchString', () => { - service.onNavigation({ 'vorgangWithEingangUrl': 'X' }); + beforeEach(() => { + vorgangFacade.getSearchInfo.mockReturnValue(hot('a', { a: searchInfo })); + }) - expect(service.setSearchInfo).toHaveBeenCalledWith(EMPTY_STRING, false); - }) - }); - }) + it('should call facade', () => { + service.getSearchInfo(); - describe('addVorgangListToVorgaenge', () => { + expect(vorgangFacade.getSearchInfo).toHaveBeenCalled(); + }) - const vorgangResource: VorgangResource = createVorgangResource(); - const additionalVorgangResource: VorgangResource = createVorgangResource(); + it.skip('FIXME: should return value', () => { + const searchInfo: Observable<SearchInfo> = service.getSearchInfo(); - beforeEach(() => { - service.vorgaenge$.next([vorgangResource]); + expect(searchInfo).toBeObservable(cold('a', { a: searchInfo })); }) + }) - it('should add vorgaenge to existing vorgaenge', () => { - const expectedVorgaenge: VorgangResource[] = [vorgangResource, additionalVorgangResource]; + describe('searchForPreview', () => { - service.addVorgangListToVorgaenge([additionalVorgangResource]); + it('should call facade', () => { + const searchString: string = 'search like me'; + service.searchForPreview(searchString); - expect(service.vorgaenge$.value).toHaveLength(2); - expect(service.vorgaenge$.value).toEqual(expectedVorgaenge); + expect(vorgangFacade.searchForPreview).toHaveBeenCalledWith(searchString); }) }) - describe('loadVorgangList', () => { + describe('getSearchPreviewList', () => { - const apiRootResource: ApiRootResource = createApiRootResource(); + describe('load required data', () => { - describe('on existing search string', () => { - - const searchString: string = 'searchParam'; + it('should get apiroot by service', () => { + service.getSearchPreviewList(); - beforeEach(() => { - repository.searchVorgaengeBy.mockReturnValue(of(vorgangListResource)); - service.searchInfo$.next(<SearchInfo>{ searchString, changedAfterSearchDone: false }); + expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); }) - it('should call repository loadVorgangList with "' + ApiRootLinkRel.SEARCH_MY_VORGAENGE + '" link', () => { - navigationService.isMyVorgaengeNavigation.mockReturnValue(true); - service.loadVorgangList(apiRootResource); + it('should get search preview list by facade', () => { + service.getSearchPreviewList(); - expect(repository.searchVorgaengeBy).toHaveBeenCalledWith(apiRootResource, searchString, ApiRootLinkRel.SEARCH_MY_VORGAENGE); + expect(vorgangFacade.getSearchPreviewList).toHaveBeenCalled(); }) - it('should call repository searchVorgaengeBy with "' + ApiRootLinkRel.SEARCH + '" link', () => { - navigationService.isMyVorgaengeNavigation.mockReturnValue(false); - - service.loadVorgangList(apiRootResource); + it('should get search info by facade', () => { + service.getSearchPreviewList(); - expect(repository.searchVorgaengeBy).toHaveBeenCalledWith(apiRootResource, searchString, ApiRootLinkRel.SEARCH); + expect(vorgangFacade.getSearchInfo).toHaveBeenCalled(); }) }) - describe('on empty search string', () => { + describe('on loaded resource', () => { - it('should call repository loadMyVorgaengeList on "myVorgange" navigation', () => { - navigationService.isMyVorgaengeNavigation.mockReturnValue(true); - repository.loadMyVorgaengeList.mockReturnValue(of(vorgangListResource)); + const vorgangListStateResource: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); + const apiRootStateResource: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); + const searchString: string = 'search like me'; + const searchInfo: SearchInfo = <any>{ searchString }; - service.loadVorgangList(apiRootResource); + beforeEach(() => { + service.searchForPreview = jest.fn(); - expect(repository.loadMyVorgaengeList).toHaveBeenCalledWith(apiRootResource); + vorgangFacade.getSearchPreviewList.mockReturnValue(hot('-a', { a: vorgangListStateResource })); + vorgangFacade.getSearchInfo.mockReturnValue(hot('-a', { a: searchInfo })); + apiRootFacade.getApiRoot.mockReturnValue(hot('-a', { a: apiRootStateResource })); }) - it('should call repository loadVorgangList on "allVorgange" navigation', () => { - navigationService.isMyVorgaengeNavigation.mockReturnValue(false); - repository.loadVorgangList.mockReturnValue(of(vorgangListResource)); + it('should return loaded resource', () => { + const vorgangList = service.getSearchPreviewList(); - service.loadVorgangList(apiRootResource); + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: vorgangListStateResource })); + }) - expect(repository.loadVorgangList).toHaveBeenCalledWith(apiRootResource); + it('should not load vorgangList', () => { + service.getSearchPreviewList(); + + expect(service.searchForPreview).not.toHaveBeenCalled(); }) }) - }) - - describe('search for preview', () => { - const SEARCH_STRING: string = 'i searched for...'; - const apiRootResource: ApiRootResource = createApiRootResource(); + describe('on reload required', () => { - describe('searchForPreview', () => { + 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(() => { - service.setVorgangSearchPreviewListLoading = jest.fn(); - apiRootFacade.getApiRoot.mockReturnValue(of(apiRootResource)); + vorgangFacade.getSearchPreviewList.mockReturnValue(hot('-a', { a: searchPreviewList })); + vorgangFacade.getSearchInfo.mockReturnValue(hot('-a', { a: searchInfo })); + apiRootFacade.getApiRoot.mockReturnValue(hot('-a', { a: apiRootStateResource })); }) - it('should set search preview list loading', () => { - service.searchForPreview(SEARCH_STRING); + it('should return value on loaded resource', () => { + const vorgangList = service.getSearchPreviewList(); - expect(service.setVorgangSearchPreviewListLoading).toHaveBeenCalled(); + expect(vorgangList).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: { ...searchPreviewList, reload: true } })); }) - it('should load apiroot', () => { - service.searchForPreview(SEARCH_STRING); - - expect(apiRootFacade.getApiRoot).toHaveBeenCalled(); - }) - }) - - describe('loadVorgangSearchPreviewList', () => { - - it('should call repository', () => { - navigationService.isMyVorgaengeNavigation.mockReturnValue(false); - - service.loadVorgangSearchPreviewList(apiRootResource, SEARCH_STRING); + it.skip('FIXME: should call facade searchForPreview', () => { + const vorgangList = service.getSearchPreviewList(); - expect(repository.searchVorgaengeBy).toHaveBeenCalledWith(apiRootResource, SEARCH_STRING, ApiRootLinkRel.SEARCH, service.SEARCH_PREVIEW_LIMIT); + 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 index b95bdd193ce68163b4061ea92ba0a7c310c1b978..53077de656eedaf4d86bcc1634decf763a567255 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/vorgang-list.service.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-list.service.ts @@ -1,201 +1,73 @@ import { Injectable } from '@angular/core'; -import { Params } from '@angular/router'; -import { ApiRootFacade, ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-shared'; -import { createEmptyStateResource, createStateResource, doIfLoadingRequired, EMPTY_STRING, isNotEmpty, isNotNil, NavigationService, StateResource } from '@goofy-client/tech-shared'; -import { hasLink } from '@ngxp/rest'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { filter, first, mergeMap } from 'rxjs/operators'; -import { VorgangListLinkRel } from './vorgang.linkrel'; +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'; -import { VorgangRepository } from './vorgang.repository'; -import { VorgangService } from './vorgang.service'; -import { getVorgaengeFromList } from './vorgang.util'; @Injectable({ providedIn: 'root' }) export class VorgangListService { - public static readonly SEARCH: string = 'search'; - - readonly vorgangList$: BehaviorSubject<StateResource<VorgangListResource>> = new BehaviorSubject<StateResource<VorgangListResource>>(createEmptyStateResource<VorgangListResource>()); - readonly vorgaenge$: BehaviorSubject<VorgangResource[]> = new BehaviorSubject<VorgangResource[]>([]); - readonly hasNextPage$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); - - readonly searchInfo$: BehaviorSubject<SearchInfo> = new BehaviorSubject<SearchInfo>({ searchString: null, changedAfterSearchDone: false });//TODO auf einfachen string umstellen - private readonly vorgangSearchPreviewList$: BehaviorSubject<StateResource<VorgangListResource>> = new BehaviorSubject<StateResource<VorgangListResource>>(createEmptyStateResource<VorgangListResource>()); - - readonly SEARCH_PREVIEW_LIMIT: number = 7; - - constructor( - private apiRootFacade: ApiRootFacade, - private vorgangRepository: VorgangRepository, - private navigationService: NavigationService - ) { - this.listenOnNavigation(); - } - - private listenOnNavigation(): void { - this.navigationService.urlChanged().subscribe(params => this.onNavigation(params)); - } - - onNavigation(params: Params): void { - if (this.navigationService.isMyVorgaengeNavigation()) { - this.setVorgangListToReload(); - } - if (NavigationService.isVorgangListPage(params)) { - this.setSearchInfo(EMPTY_STRING, false); - this.reloadIfNeccessary(); - } - if (NavigationService.isSearch(params)) { - this.setSearchInfo(this.navigationService.getDecodedUriParam(VorgangListService.SEARCH), true); - this.clearVorgangSearchPreviewList(); - this.reloadIfNeccessary(); - } - if (NavigationService.isVorgangDetailPage(params, VorgangService.VORGANG_WITH_EINGANG_URL)) { - this.setSearchInfo(EMPTY_STRING, false); - } - } - - setSearchInfo(searchString: string, changedAfterSearchDone: boolean): void { - this.searchInfo$.next({ ...this.searchInfo$.value, searchString, changedAfterSearchDone }); - } - - reloadIfNeccessary(): void { - if (this.isVorgangListLoaded()) { - this.reloadVorgangList(); - } - } - - isVorgangListLoaded(): boolean { - return this.vorgangList$.value.loaded; - } - - public getVorgaenge(): Observable<VorgangResource[]> { - return this.vorgaenge$; - } - - public hasNextPage(): Observable<boolean> { - return this.hasNextPage$; - } - - setVorgangListToReload(): void { - this.vorgangList$.next({ ...this.vorgangList$.value, reload: true }); - } - - public reloadVorgangList(): void { - this.setVorgangListToReload(); - this.getVorgangList(); - } + constructor(private vorgangFacade: VorgangFacade, private apiRootFacade: ApiRootFacade, private navigationFacade: NavigationFacade) { } public getVorgangList(): Observable<StateResource<VorgangListResource>> { - doIfLoadingRequired(this.vorgangList$.value, () => this.apiRootFacade.getApiRoot().pipe( - filter(apiRootResource => apiRootResource.loaded), - mergeMap(apiRootResource => this.loadVorgangList(apiRootResource.resource) - )).subscribe(vorgangList => { - if (vorgangList !== null) { - this.updateVorgangList(vorgangList); - this.updateVorgaenge(getVorgaengeFromList(vorgangList)); + 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)); } - })); - - return this.vorgangList$; + }), + map(([vorgangList, ,]) => vorgangList), + startWith(createEmptyStateResource<VorgangListResource>(true))); } - loadVorgangList(apiRootResource: ApiRootResource): Observable<VorgangListResource> { - this.setVorgangListLoading(); - - if (isNotEmpty(this.searchInfo$.value.searchString)) { - return this.vorgangRepository.searchVorgaengeBy(apiRootResource, this.searchInfo$.value.searchString, this.getSearchLinkRel()); + loadVorgangList(apiRoot: ApiRootResource, currentRouteData: RouteData): void { + if (isMyVorgaenge(currentRouteData)) { + this.vorgangFacade.loadMyVorgaengeList(apiRoot); } - if (this.navigationService.isMyVorgaengeNavigation()) { - return this.vorgangRepository.loadMyVorgaengeList(apiRootResource); + if (isVorgangListPage(currentRouteData)) { + this.vorgangFacade.loadVorgangList(apiRoot); } - - return this.vorgangRepository.loadVorgangList(apiRootResource); - } - - public loadNextPage(): void { - if (this.isVorgangListLoading()) { - return; + if (isSearch(currentRouteData)) { + this.vorgangFacade.searchVorgaengeBy(apiRoot, getSearchString(currentRouteData), getSearchLinkRel(currentRouteData)); } - this.loadNextListPage(); - } - - private isVorgangListLoading(): boolean { - return this.vorgangList$.value.loading; - } - - private loadNextListPage(): void { - this.setVorgangListLoading(); - - this.vorgangRepository.getNextVorgangListPage(this.vorgangList$.value.resource).pipe(first()).subscribe(vorgangList => { - if (vorgangList !== null) { - this.updateVorgangList(vorgangList); - this.addVorgangListToVorgaenge(getVorgaengeFromList(vorgangList)); - } - }); - } - - private setVorgangListLoading(): void { - this.vorgangList$.next({ ...this.vorgangList$.value, loading: true }) - } - - private updateVorgangList(vorgangList: VorgangListResource): void { - this.vorgangList$.next(createStateResource(vorgangList)); - this.updateHasNextPage(); } - private updateHasNextPage(): void { - this.hasNextPage$.next(hasLink(this.vorgangList$.value.resource, VorgangListLinkRel.NEXT)); + public clearSearchPreviewList(): void { + this.vorgangFacade.clearSearchPreviewList(); } - addVorgangListToVorgaenge(vorgaenge: VorgangResource[]): void { - const updatedVorgaenge: VorgangResource[] = [...this.vorgaenge$.value].concat(vorgaenge); - - this.updateVorgaenge(updatedVorgaenge); - } - - private updateVorgaenge(vorgaenge: VorgangResource[]): void { - this.vorgaenge$.next(vorgaenge); + public getVorgaenge(): Observable<VorgangResource[]> { + return this.vorgangFacade.getVorgaenge(); } - getSearchInfo(): Observable<SearchInfo> { - return this.searchInfo$.asObservable(); + public loadNextPage(): void { + this.vorgangFacade.loadNextPage(); } - public getVorgangSearchPreviewList(): Observable<StateResource<VorgangListResource>> { - return this.vorgangSearchPreviewList$.asObservable(); + public getSearchInfo(): Observable<SearchInfo> { + return this.vorgangFacade.getSearchInfo(); } public searchForPreview(searchString: string): void { - this.setVorgangSearchPreviewListLoading(); - - this.apiRootFacade.getApiRoot().pipe( - filter(apiRootResource => apiRootResource.loaded), - mergeMap(apiRootResource => this.loadVorgangSearchPreviewList(apiRootResource.resource, searchString))) - .subscribe(vorgangList => { - if (isNotNil(vorgangList)) { - this.updateVorgangSearchPreviewList(vorgangList); - } - }); + this.vorgangFacade.searchForPreview(searchString); } - setVorgangSearchPreviewListLoading() { - this.vorgangSearchPreviewList$.next({ ...this.vorgangSearchPreviewList$.value, loading: true }); - } - - loadVorgangSearchPreviewList(apiRootResource: ApiRootResource, searchString: string): Observable<VorgangListResource> { - return this.vorgangRepository.searchVorgaengeBy(apiRootResource, searchString, this.getSearchLinkRel(), this.SEARCH_PREVIEW_LIMIT) - } - - private getSearchLinkRel(): string { - return this.navigationService.isMyVorgaengeNavigation() ? ApiRootLinkRel.SEARCH_MY_VORGAENGE : ApiRootLinkRel.SEARCH; - } - - private updateVorgangSearchPreviewList(vorgangList: VorgangListResource): void { - this.vorgangSearchPreviewList$.next(createStateResource(vorgangList)); + 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))); } - public clearVorgangSearchPreviewList(): void { - this.vorgangSearchPreviewList$.next(createEmptyStateResource<VorgangListResource>()); + 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 new file mode 100644 index 0000000000000000000000000000000000000000..64e15fbad865dd3b7f398ebe5cff0ce869428551 --- /dev/null +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.spec.ts @@ -0,0 +1,135 @@ +import { UrlSegment } from '@angular/router'; +import { ApiRootLinkRel } from '@goofy-client/api-root-shared'; +import { RouteData } from '@goofy-client/navigation-shared'; +import { createRouteData } from 'libs/navigation-shared/test/navigation-test-factory'; +import { VorgangEffects } from './+state/vorgang.effects'; +import { getSearchLinkRel, getSearchString, isMyVorgaenge, isSearch, isVorgangDetailPage, isVorgangListPage } from './vorgang-navigation.util'; + +describe('Vorgang Navigation Util', () => { + + describe('isSearch', () => { + + it('should return true if search parameter exists', () => { + const queryParameter = { [VorgangEffects.SEARCH_QUERY_PARAM]: 'searchThisForMe' }; + const currentRouteData: RouteData = { ...createRouteData(), queryParameter }; + + const exists: boolean = isSearch(currentRouteData); + + expect(exists).toBeTruthy(); + }) + + it('should return false if NOT exists', () => { + const exists: boolean = isSearch(createRouteData()); + + expect(exists).toBeFalsy(); + }) + }) + + describe('getSearchString', () => { + + it('should return searchString', () => { + const searchString: string = 'searchThisForMe'; + + const result = getSearchString(buildCurrentRouteData(searchString)); + + expect(result).toBe(searchString); + }) + + it('should return decoded searchString', () => { + const searchString: string = 'search This For Me'; + + const result = getSearchString(buildCurrentRouteData(encodeURIComponent(searchString))); + + expect(result).toBe(searchString); + }) + + 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 }; + } + }) + + describe('getSearchLinkRel', () => { + + it('should return "searchMyVorgaenge" linkrel', () => { + const urlSegments: UrlSegment[] = [<any>{ path: VorgangEffects.MY_VORGAENGE_URI_SEGMENT }]; + const currentRouteData: RouteData = { ...createRouteData(), urlSegments }; + + const linkRel: string = getSearchLinkRel(currentRouteData); + + expect(linkRel).toBe(ApiRootLinkRel.SEARCH_MY_VORGAENGE); + }) + + it('should return "search" linkrel', () => { + const linkRel: string = getSearchLinkRel(createRouteData()); + + expect(linkRel).toBe(ApiRootLinkRel.SEARCH); + }) + }) + + describe('isMyVorgaenge', () => { + + it('should return true if "myVorgaenge" exists in urlSegments', () => { + const urlSegments: UrlSegment[] = [<any>{ path: VorgangEffects.MY_VORGAENGE_URI_SEGMENT }]; + const currentRouteData: RouteData = { ...createRouteData(), urlSegments }; + + const result: boolean = isMyVorgaenge(currentRouteData); + + expect(result).toBeTruthy(); + }) + + it('should return false if urlSegments is empty', () => { + 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 = { ...createRouteData(), urlSegments }; + + const result: boolean = isMyVorgaenge(currentRouteData); + + expect(result).toBeFalsy(); + }) + }) + + describe('isVorgangListPage', () => { + + it('should return true if queryParameter is empty', () => { + const result: boolean = isVorgangListPage(createRouteData()); + + 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 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(); + }) + + it('should return false if queryParameter NOT contains "vorgangWithEingangUrl"', () => { + const result: boolean = isVorgangDetailPage({ ...createRouteData(), queryParameter: { ['search']: 'exampleParameter' } }); + + expect(result).toBeFalsy(); + }) + }) +}) \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..181b62b0b15b7539889258416a99bf8fe3a9e76b --- /dev/null +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang-navigation.util.ts @@ -0,0 +1,41 @@ +import { ApiRootLinkRel } from '@goofy-client/api-root-shared'; +import { RouteData } from '@goofy-client/navigation-shared'; +import { isEmptyObject, isNotUndefined } from '@goofy-client/tech-shared'; +import { VorgangEffects } from './+state/vorgang.effects'; + +export function isSearch(routeData: RouteData): boolean { + return isNotUndefined(routeData.queryParameter[VorgangEffects.SEARCH_QUERY_PARAM]); +} + +export function getSearchString(routeData: RouteData): string { + const searchString: string = routeData.urlSegments[getSearchStringIndex(routeData) + 1].path.toString(); + return decodeURIComponent(searchString); +} + +function getSearchStringIndex(routeData: RouteData): number { + return routeData.urlSegments.findIndex(segment => segment.path.toString() == VorgangEffects.SEARCH_QUERY_PARAM); +} + +export function getSearchLinkRel(routeData: RouteData): string { + return containsMyVorgaenge(routeData) ? ApiRootLinkRel.SEARCH_MY_VORGAENGE : ApiRootLinkRel.SEARCH; +} + +export function isMyVorgaenge(routeData: RouteData): boolean { + return getPrimarySegmentPath(routeData) == VorgangEffects.MY_VORGAENGE_URI_SEGMENT && !isSearch(routeData); +} + +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-shared/src/lib/vorgang.repository.ts b/goofy-client/libs/vorgang-shared/src/lib/vorgang.repository.ts index 7c8bca38c45a2d02399588f49234902823ade344..3cd57efc5f4081718a5852034d2704e3190ef7fb 100644 --- a/goofy-client/libs/vorgang-shared/src/lib/vorgang.repository.ts +++ b/goofy-client/libs/vorgang-shared/src/lib/vorgang.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-shared'; import { BinaryFileListResource } from '@goofy-client/binary-file-shared'; -import { CatchHttpError, EMPTY_STRING } from '@goofy-client/tech-shared'; +import { CatchHttpError, EMPTY_STRING, SkipInterceptor } from '@goofy-client/tech-shared'; import { getUrl, Resource, ResourceFactory, ResourceUri } from '@ngxp/rest'; import { Observable } from 'rxjs'; import { VorgangListLinkRel, VorgangWithEingangLinkRel } from './vorgang.linkrel'; @@ -43,6 +43,7 @@ export class VorgangRepository { return this.resourceFactory.from(vorgang).get(VorgangWithEingangLinkRel.REPRESENTATIONS); } + @SkipInterceptor() public searchVorgaengeBy(apiRootResource: ApiRootResource, searchBy: string, linkRel: string, limit?: number): Observable<VorgangListResource> { return this.resourceFactory.fromId(this.buildSearchByUrl(apiRootResource, searchBy, linkRel, limit)).get(); } diff --git a/goofy-client/libs/vorgang-shared/test/vorgang.ts b/goofy-client/libs/vorgang-shared/test/vorgang.ts index e520bed3286faec5213504b18f792cd8922adfe8..08e841babed9b6b61513ccf8da919693fd714985 100644 --- a/goofy-client/libs/vorgang-shared/test/vorgang.ts +++ b/goofy-client/libs/vorgang-shared/test/vorgang.ts @@ -102,6 +102,12 @@ export function createVorgangListResource(linkRelations: string[] = []): Vorgang }); } +export function createVorgangListResourceWithResource(resources: VorgangResource[], linkRelations: string[] = []): VorgangListResource { + return toResource({}, [...linkRelations], { + [VorgangListLinkRel.VORGANG_HEADER_LIST]: resources + }); +} + export function createVorgangWithEingangResource(linkRelations: string[] = []): VorgangWithEingangResource { return toResource(createVorgangWithEingang(), linkRelations); } diff --git a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.html b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.html index 8edf645c0c7069b63e3b866ecc7db3669488a0b4..1c34a9ff7d5a328c33cbe02c857467ed6322e283 100644 --- a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.html +++ b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list-container.component.html @@ -1,6 +1,5 @@ <ng-container *ngIf="{ vorgaenge: vorgaenge$ | async, - hasNextPage: hasNextPage$ | async, vorgangListPageResource: vorgangListPageResource$ | async } as vorgangListData"> @@ -8,7 +7,6 @@ class="l-scroll-area--full" [vorgangListPageResource]="vorgangListData.vorgangListPageResource" [vorgaenge]="vorgangListData.vorgaenge" - [hasNextPage]="vorgangListData.hasNextPage" [searchString]="searchString$ | async" (nextPage)="loadNextPage()" data-test-id="vorgang-list"> 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 a506b111ee69a0cc6cea9c1aadc77cbddd1a1203..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 @@ -11,7 +11,8 @@ describe('VorgangListContainerComponent', () => { let component: VorgangListContainerComponent; let fixture: ComponentFixture<VorgangListContainerComponent>; - const vorgangListService = { ...mock(VorgangListService), getSearchInfo: () => of(<SearchInfo>{ searchString: EMPTY_STRING, changedAfterSearchDone: false }) }; + const searchInfo: SearchInfo = { searchString: EMPTY_STRING, changedAfterSearchDone: false }; + const vorgangListService = { ...mock(VorgangListService), getSearchInfo: jest.fn().mockReturnValue(of(searchInfo)) }; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -24,7 +25,7 @@ describe('VorgangListContainerComponent', () => { provide: VorgangListService, useValue: vorgangListService } - ], + ] }) }); @@ -38,9 +39,30 @@ describe('VorgangListContainerComponent', () => { expect(component).toBeTruthy(); }); + describe('ngOnInit', () => { + + it('should call facade to get vorgaenge', () => { + component.ngOnInit(); + + expect(vorgangListService.getVorgaenge).toHaveBeenCalled(); + }) + + it('should call facade to get vorgangList', () => { + component.ngOnInit(); + + expect(vorgangListService.getVorgangList).toHaveBeenCalled(); + }) + + it('should call facade to get searchInfo', () => { + component.ngOnInit(); + + expect(vorgangListService.getSearchInfo).toHaveBeenCalled(); + }) + }) + describe('load next page', () => { - it('should call vorgang list service loadNextPage', () => { + it('should call facade loadNextPage', () => { component.loadNextPage(); 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 ffc6d58f8d03b0f72b0be4be2195b3cafc3cc6e7..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,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { StateResource } from '@goofy-client/tech-shared'; import { VorgangListResource, VorgangListService, VorgangResource } from '@goofy-client/vorgang-shared'; import { Observable } from 'rxjs'; @@ -9,16 +9,16 @@ import { map } from 'rxjs/operators'; templateUrl: './vorgang-list-container.component.html', styleUrls: ['./vorgang-list-container.component.scss'] }) -export class VorgangListContainerComponent { +export class VorgangListContainerComponent implements OnInit { public vorgangListPageResource$: Observable<StateResource<VorgangListResource>>; public vorgaenge$: Observable<VorgangResource[]> - public hasNextPage$: Observable<boolean>; public searchString$: Observable<string>; - constructor(private vorgangListService: VorgangListService) { + constructor(private vorgangListService: VorgangListService) { } + + ngOnInit(): void { this.vorgaenge$ = this.vorgangListService.getVorgaenge(); - this.hasNextPage$ = this.vorgangListService.hasNextPage(); this.vorgangListPageResource$ = this.vorgangListService.getVorgangList(); this.searchString$ = this.vorgangListService.getSearchInfo().pipe(map(searchInfo => searchInfo.searchString)); } diff --git a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.html b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.html index f12c998432bd4b2ecc320e99b232bbca69c9d940..7222e2d59bd2beb2eaa4c585d8f89333b7979fbd 100644 --- a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.html +++ b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.html @@ -2,8 +2,6 @@ <goofy-client-spinner diameter="60" [stateResource]="vorgangListPageResource"></goofy-client-spinner> -<goofy-client-empty-list - *ngIf="!vorgangListPageResource.loading && !(vorgaenge && vorgaenge.length)" - [searchString]="searchString" - data-test-id="empty-list"> +<goofy-client-empty-list *ngIf="!vorgangListPageResource.loading && !(vorgaenge && vorgaenge.length)" data-test-id="empty-list" + [searchString]="searchString"> </goofy-client-empty-list> diff --git a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.spec.ts b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.spec.ts index 9dfcfc2d9dfebc2443580e4f28af0281f1e7b5d9..7ae84958afa3e3072836297873c9e2577a3f44f0 100644 --- a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.spec.ts +++ b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.spec.ts @@ -1,9 +1,13 @@ import { ScrollingModule } from '@angular/cdk/scrolling'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createEmptyStateResource } from '@goofy-client/tech-shared'; +import { createEmptyStateResource, createStateResource } from '@goofy-client/tech-shared'; +import { getElementFromFixture, getElementsFromFixture, mock } from '@goofy-client/test-utils'; +import { VorgangListLinkRel } from '@goofy-client/vorgang-shared'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { SpinnerComponent } from 'libs/ui/src/lib/ui/spinner/spinner.component'; -import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; +import { createVorgangListResource, createVorgangResources } from 'libs/vorgang-shared/test/vorgang'; import { MockComponent } from 'ng-mocks'; +import { EventEmitter } from 'stream'; import { EmptyListComponent } from './empty-list/empty-list.component'; import { VorgangListItemComponent } from './vorgang-list-item/vorgang-list-item.component'; import { VorgangListComponent } from './vorgang-list.component'; @@ -13,7 +17,7 @@ describe('VorgangListComponent', () => { let fixture: ComponentFixture<VorgangListComponent>; const vorgangListItem: string = 'goofy-client-vorgang-list-item'; - const emptyList: string = '[data-test-id="empty-list"]'; + const emptyList: string = getDataTestIdOf('empty-list'); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -33,7 +37,7 @@ describe('VorgangListComponent', () => { fixture = TestBed.createComponent(VorgangListComponent); component = fixture.componentInstance; component.vorgangListPageResource = createEmptyStateResource(); - component.vorgaenge = [createVorgangResource(), createVorgangResource()]; + component.vorgaenge = createVorgangResources(); fixture.detectChanges(); }); @@ -44,17 +48,82 @@ describe('VorgangListComponent', () => { it('should have vorgang-list-item', () => { fixture.detectChanges(); - const elements = fixture.nativeElement.querySelectorAll(vorgangListItem); + const elements = getElementsFromFixture(fixture, vorgangListItem); - expect(elements).toHaveLength(2); + expect(elements).toHaveLength(10); }); it('should have empty list', () => { component.vorgaenge = []; fixture.detectChanges(); - const emptyListElement = fixture.nativeElement.querySelector(emptyList); + const emptyListElement = getElementFromFixture(fixture, emptyList); expect(emptyListElement).toBeInstanceOf(HTMLElement); }); -}); + + describe('onWindowsScroll', () => { + + beforeEach(() => { + component.loadNextPage = jest.fn(); + }) + + it('should call loadNextPage if scrolled to bottom', () => { + component.isScrolledToBottom = jest.fn().mockReturnValue(true); + + component.onWindowScroll({}); + + expect(component.loadNextPage).toHaveBeenCalled(); + }) + + it('should do nothing if NOT scrolled to bottom', () => { + component.isScrolledToBottom = jest.fn().mockReturnValue(false); + + component.onWindowScroll({}); + + expect(component.loadNextPage).not.toHaveBeenCalled(); + }) + }) + + describe('load next page', () => { + + beforeEach(() => { + component.nextPage = <any>mock(EventEmitter); + }) + + it('should emit "nextPage" if necesarry', () => { + component.shouldLoadNextPage = jest.fn().mockReturnValue(true); + + component.loadNextPage(); + + expect(component.nextPage.emit).toHaveBeenCalled(); + }) + + it('should emit "nextPage" if necesarry', () => { + component.shouldLoadNextPage = jest.fn().mockReturnValue(false); + + component.loadNextPage(); + + expect(component.nextPage.emit).not.toHaveBeenCalled(); + }) + }) + + describe('has next page', () => { + + it('should return true is link exists', () => { + component.vorgangListPageResource = createStateResource(createVorgangListResource([VorgangListLinkRel.NEXT])); + + const hasNextPage: boolean = component.hasNextPage(); + + expect(hasNextPage).toBeTruthy(); + }) + + it('should return false if link NOT exists', () => { + component.vorgangListPageResource = createStateResource(createVorgangListResource()); + + const hasNextPage: boolean = component.hasNextPage(); + + expect(hasNextPage).toBeFalsy(); + }) + }) +}); \ No newline at end of file diff --git a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.ts b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.ts index 9cb06d55d0d0b01dd0c9de233d2634bf5f9a1cbe..9f95f07878122af52ef2cc42c3276dcf427087de 100644 --- a/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.ts +++ b/goofy-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, HostListener, Input, Output } from '@angular/core'; import { StateResource } from '@goofy-client/tech-shared'; -import { VorgangListResource, VorgangResource } from '@goofy-client/vorgang-shared'; +import { VorgangListLinkRel, VorgangListResource, VorgangResource } from '@goofy-client/vorgang-shared'; +import { hasLink } from '@ngxp/rest'; @Component({ selector: 'goofy-client-vorgang-list', @@ -12,30 +13,38 @@ export class VorgangListComponent { @Input() vorgangListPageResource: StateResource<VorgangListResource>; @Input() vorgaenge: VorgangResource[]; @Input() searchString: string; - @Input() hasNextPage: boolean; - @Output() private nextPage: EventEmitter<void> = new EventEmitter<void>(); + @Output() nextPage: EventEmitter<void> = new EventEmitter<void>(); @HostListener('window:scroll', ['$event']) - onWindowScroll(event) { - try { - const top = event.currentTarget.scrollY; - const height = document.documentElement.scrollHeight; - const innerHeight = event.currentTarget.innerHeight + 1; - - if (top >= height - innerHeight) { - this.nextBatch(); - } - } catch (error) { } + onWindowScroll(event) {//TODO event typisieren?! + if (this.isScrolledToBottom(event)) { + this.loadNextPage(); + } + } + + isScrolledToBottom(event): boolean { + const top = event.currentTarget.scrollY; + const height = document.documentElement.scrollHeight; + const innerHeight = event.currentTarget.innerHeight + 1; + return top >= height - innerHeight; } - private nextBatch(): void { + loadNextPage(): void { if (this.shouldLoadNextPage()) { this.nextPage.emit(); } } - private shouldLoadNextPage(): boolean { - return !this.vorgangListPageResource.loading && this.hasNextPage; + shouldLoadNextPage(): boolean { + return this.isVorgangListNotLoading() && this.hasNextPage() + } + + private isVorgangListNotLoading(): boolean { + return !this.vorgangListPageResource.loading; + } + + hasNextPage(): boolean { + return hasLink(this.vorgangListPageResource.resource, VorgangListLinkRel.NEXT) } } diff --git a/goofy-client/libs/wiedervorlage-shared/src/lib/wiedervorlage.model.ts b/goofy-client/libs/wiedervorlage-shared/src/lib/wiedervorlage.model.ts index 540036891941a349083494fff591aa67c355a97d..d0aac314c1422267fac594600dc5d40f8ac780ed 100644 --- a/goofy-client/libs/wiedervorlage-shared/src/lib/wiedervorlage.model.ts +++ b/goofy-client/libs/wiedervorlage-shared/src/lib/wiedervorlage.model.ts @@ -8,7 +8,7 @@ export interface Wiedervorlage { beschreibung: string; frist: Date; createdAt: Date; - attachments: ResourceUri[] + attachments: ResourceUri[] | string } export interface WiedervorlageResource extends Wiedervorlage, Resource { } diff --git a/goofy-client/libs/wiedervorlage-shared/test/wiedervorlage.ts b/goofy-client/libs/wiedervorlage-shared/test/wiedervorlage.ts index a934133ccbc64b8059b15cf8b2af87dc81cafff3..9bc1b5409c49a2abacf62be594efbfb765c01d52 100644 --- a/goofy-client/libs/wiedervorlage-shared/test/wiedervorlage.ts +++ b/goofy-client/libs/wiedervorlage-shared/test/wiedervorlage.ts @@ -17,7 +17,7 @@ export function createWiedervorlage(): Wiedervorlage { beschreibung: faker.lorem.words(12), frist: faker.date.between(past, future), createdAt: faker.date.past(), - attachments: [faker.internet.url()] + attachments: faker.internet.url() } } diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionController.java b/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionController.java index 13b0beda9c0c794ea29ad055d510d1e7c5d7bbe7..b89a7883fbd996a3c687f30902808236ede764aa 100644 --- a/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionController.java +++ b/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionController.java @@ -57,7 +57,7 @@ public class GrpcExceptionController { return buildAccessDeniedErrorResponse(e); } - return buildInternalUnavaibleErrorResponse(e); + return buildInternalUnavailableErrorResponse(e); } private boolean isNotFoundStatusCode(StatusRuntimeException e) { @@ -73,7 +73,11 @@ public class GrpcExceptionController { return new ResponseEntity<>(buildApiErrorWithIssue(issueBuilder.build()), HttpStatus.NOT_FOUND); } - private ResponseEntity<ApiError> buildInternalUnavaibleErrorResponse(StatusRuntimeException e) { + private ResponseEntity<ApiError> buildInternalUnavailableErrorResponse(StatusRuntimeException e) { + if (hasExceptionId(e)) { + return new ResponseEntity<>(buildInternalUnavailableApiError(e, getExceptionId(e)), HttpStatus.SERVICE_UNAVAILABLE); + } + var exceptionId = createExceptionId(); var messageWithExceptionId = ExceptionUtil.formatMessageWithExceptionId(e.getMessage(), exceptionId); LOG.error("Grpc service unavailable: {}", messageWithExceptionId); diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionControllerTest.java b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionControllerTest.java index 9f5a94c3f8fc9ff2b929dfacd60c69e3fdb7392c..48a08363c8b849b8707b6efc6204b2ebcaba5442 100644 --- a/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionControllerTest.java +++ b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionControllerTest.java @@ -1,13 +1,13 @@ package de.itvsh.goofy.common.errorhandling; -import static org.assertj.core.api.Assertions.*; - import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import static org.assertj.core.api.Assertions.*; + import io.grpc.StatusRuntimeException; class GrpcExceptionControllerTest { @@ -169,6 +169,25 @@ class GrpcExceptionControllerTest { private ResponseEntity<ApiError> handleUnavailableException() { return handleException(unavailableException); } + + @Nested + class TestWithMetadata { + + private final StatusRuntimeException unavailableExceptionWithMetadata = GrpcExceptionTestFactory + .createGrpcUnavailableStatusException(GrpcExceptionTestFactory.createMetaData()); + + @Test + void shouldNotHaveExceptionId() { + var response = handleUnavailableException(); + + assertThat(response.getBody().getIssues()).hasSize(1); + assertThat(response.getBody().getIssues().get(0).getExceptionId()).isEqualTo(GrpcExceptionTestFactory.METADATA_EXCEPTION_ID_VALUE); + } + + private ResponseEntity<ApiError> handleUnavailableException() { + return handleException(unavailableExceptionWithMetadata); + } + } } @Nested diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionTestFactory.java b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionTestFactory.java index 6833309c660e2d082686fdb307236719fd2789d2..78981013ec6284b150e369da428685307ca9da34 100644 --- a/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionTestFactory.java +++ b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionTestFactory.java @@ -17,6 +17,10 @@ public class GrpcExceptionTestFactory { public static final String METADATA_ERROR_CODE_VALUE = "Error_Code"; public static final String METADATA_EXCEPTION_ID_VALUE = "42"; + public static StatusRuntimeException createGrpcUnavailableStatusException(Metadata metadata) { + return new StatusRuntimeException(buildUnavailableStatus(), metadata); + } + public static StatusRuntimeException createGrpcUnavailableStatusException() { return new StatusRuntimeException(buildUnavailableStatus()); }