From b0ff9d1cf925365ede88198824c5893c04b1c7b9 Mon Sep 17 00:00:00 2001 From: elinuxhenrik Date: Fri, 9 Apr 2021 10:36:56 +0200 Subject: [PATCH] Fix PolicyInstanceComponent and add test coverage Fix so that the instance table is correctly loaded. Change so that the table is not reloaded when the PolicyInstanceDialog is closed without submitting the policy. Change-Id: I883f0a0b42294b18ba0f4d1228c07d78609932ad Issue-ID: NONRTRIC-472 Signed-off-by: elinuxhenrik --- .../src/app/mock/policy-instance-2.json | 2 +- .../policy-instance-dialog.component.spec.ts | 48 +- .../policy-instance-dialog.component.ts | 13 +- .../policy-instance/policy-instance.component.html | 36 +- .../policy-instance/policy-instance.component.scss | 34 +- .../policy-instance.component.spec.ts | 552 +++++++++++++++++++-- .../policy-instance/policy-instance.component.ts | 192 +++---- .../policy/ric-selector/ric-selector.component.ts | 2 - 8 files changed, 674 insertions(+), 205 deletions(-) diff --git a/webapp-frontend/src/app/mock/policy-instance-2.json b/webapp-frontend/src/app/mock/policy-instance-2.json index 69ab3a4..c268625 100644 --- a/webapp-frontend/src/app/mock/policy-instance-2.json +++ b/webapp-frontend/src/app/mock/policy-instance-2.json @@ -11,7 +11,7 @@ "priorityLevel": 3100 } }, - "service_id": "service1", + "service_id": "service2", "transient": false, "status_notification_uri": "" } \ No newline at end of file diff --git a/webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.spec.ts b/webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.spec.ts index d2b1a61..8881b64 100644 --- a/webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.spec.ts +++ b/webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.spec.ts @@ -39,7 +39,6 @@ import { ToastrModule } from "ngx-toastr"; import { MockComponent } from "ng-mocks"; import { PolicyService } from "@services/policy/policy.service"; -import { ErrorDialogService } from "@services/ui/error-dialog.service"; import { UiService } from "@services/ui/ui.service"; import { PolicyInstanceDialogComponent } from "./policy-instance-dialog.component"; import { TypedPolicyEditorComponent } from "@policy/typed-policy-editor/typed-policy-editor.component"; @@ -48,26 +47,24 @@ import { NoTypePolicyEditorComponent } from "@policy/no-type-policy-editor/no-ty import { CreatePolicyInstance } from "@interfaces/policy.types"; import { NotificationService } from "@services/ui/notification.service"; import * as uuid from "uuid"; +import { HttpErrorResponse } from "@angular/common/http"; describe("PolicyInstanceDialogComponent", () => { const untypedSchema = JSON.parse("{}"); - const typedSchema = - JSON.parse('{ "description": "Type 1 policy type", "title": "1", "type": "object", "properties": { "priorityLevel": "number" }}'); + const typedSchema = JSON.parse( + '{ "description": "Type 1 policy type", "title": "1", "type": "object", "properties": { "priorityLevel": "number" }}' + ); let component: PolicyInstanceDialogComponent; let fixture: ComponentFixture; let loader: HarnessLoader; let dialogRefSpy: MatDialogRef; let policyServiceSpy: jasmine.SpyObj; - let errDialogServiceSpy: jasmine.SpyObj; let notificationServiceSpy: NotificationService; beforeEach(async () => { dialogRefSpy = jasmine.createSpyObj("MatDialogRef", ["close"]); policyServiceSpy = jasmine.createSpyObj("PolicyService", ["putPolicy"]); - errDialogServiceSpy = jasmine.createSpyObj("ErrorDialogService", [ - "displayError", - ]); notificationServiceSpy = jasmine.createSpyObj("NotificationService", [ "success", ]); @@ -93,7 +90,6 @@ describe("PolicyInstanceDialogComponent", () => { ChangeDetectorRef, { provide: MatDialogRef, useValue: dialogRefSpy }, { provide: PolicyService, useValue: policyServiceSpy }, - { provide: ErrorDialogService, useValue: errDialogServiceSpy }, { provide: NotificationService, useValue: notificationServiceSpy }, { provide: MAT_DIALOG_DATA, useValue: true }, UiService, @@ -199,7 +195,7 @@ describe("PolicyInstanceDialogComponent", () => { expect(await submitButton.isDisabled()).toBeFalsy(); }); - it("should generate policy ID when submitting new policy", async () => { + it("should generate policy ID when submitting new policy and close dialog", async () => { const ricSelector: RicSelectorComponent = fixture.debugElement.query( By.directive(RicSelectorComponent) ).componentInstance; @@ -213,6 +209,9 @@ describe("PolicyInstanceDialogComponent", () => { spyOn(uuid, "v4").and.returnValue("1234567890"); ricSelector.selectedRic.emit("ric1"); noTypePolicyEditor.validJson.emit("{}"); + + policyServiceSpy.putPolicy.and.returnValue(of("Success")); + await submitButton.click(); const policyInstance = {} as CreatePolicyInstance; @@ -222,6 +221,26 @@ describe("PolicyInstanceDialogComponent", () => { policyInstance.ric_id = "ric1"; policyInstance.service_id = "controlpanel"; expect(policyServiceSpy.putPolicy).toHaveBeenCalledWith(policyInstance); + + expect(dialogRefSpy.close).toHaveBeenCalledWith("ok"); + }); + + it("should not close dialog when error from server", async () => { + let submitButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#submitButton" }) + ); + + const errorResponse = { + status: 400, + statusText: "Bad Request", + } as HttpErrorResponse; + policyServiceSpy.putPolicy.and.returnValue(errorResponse); + + await submitButton.click(); + + expect(policyServiceSpy.putPolicy).toHaveBeenCalled(); + + expect(dialogRefSpy.close).not.toHaveBeenCalled(); }); }); @@ -308,7 +327,9 @@ describe("PolicyInstanceDialogComponent", () => { }); describe("content when editing policy without type", () => { - const instanceJson = JSON.parse('{"qosObjectives": {"priorityLevel": 3100}}'); + const instanceJson = JSON.parse( + '{"qosObjectives": {"priorityLevel": 3100}}' + ); beforeEach(async () => { const policyData = { createSchema: untypedSchema, @@ -349,9 +370,7 @@ describe("PolicyInstanceDialogComponent", () => { By.directive(NoTypePolicyEditorComponent) ).componentInstance; expect(noTypePolicyEditor).toBeTruthy(); - expect(noTypePolicyEditor.policyJson).toEqual( - instanceJson - ); + expect(noTypePolicyEditor.policyJson).toEqual(instanceJson); }); it("should contain enabled Close and Submit buttons when all inputs are valid", async () => { @@ -481,7 +500,8 @@ function policyTester(first, second) { return ( typeof policy1.policy_data === "object" && typeof policy2.policy_data === "object" && - JSON.stringify(policy1.policy_data) === JSON.stringify(policy2.policy_data) && + JSON.stringify(policy1.policy_data) === + JSON.stringify(policy2.policy_data) && policy1.policy_id === policy2.policy_id && policy1.policytype_id === policy2.policytype_id && policy1.ric_id === policy2.ric_id && diff --git a/webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.ts b/webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.ts index 05604e3..8750d42 100644 --- a/webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.ts +++ b/webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.ts @@ -32,8 +32,6 @@ import { import { PolicyService } from "@services/policy/policy.service"; import { NotificationService } from "@services/ui/notification.service"; import { UiService } from "@services/ui/ui.service"; -import { HttpErrorResponse } from "@angular/common/http"; -import { ErrorDialogService } from "@services/ui/error-dialog.service"; import * as uuid from "uuid"; import { CreatePolicyInstance, @@ -57,7 +55,6 @@ export class PolicyInstanceDialogComponent implements OnInit, AfterViewInit { private cdr: ChangeDetectorRef, public dialogRef: MatDialogRef, private policySvc: PolicyService, - private errorService: ErrorDialogService, private notificationService: NotificationService, @Inject(MAT_DIALOG_DATA) private data, private ui: UiService @@ -100,10 +97,7 @@ export class PolicyInstanceDialogComponent implements OnInit, AfterViewInit { self.notificationService.success( "Policy " + self.policyInstance.policy_id + " submitted" ); - self.dialogRef.close(); - }, - error(error: HttpErrorResponse) { - self.errorService.displayError("Submit failed: " + error.error); + self.dialogRef.close("ok"); }, complete() {}, }); @@ -131,7 +125,7 @@ export function getPolicyDialogProperties( const instanceJson = instance ? instance.policy_data : null; const name = policyTypeSchema.name; const ric = instance ? instance.ric_id : null; - return { + const data = { maxWidth: "1200px", maxHeight: "900px", width: "900px", @@ -145,5 +139,6 @@ export function getPolicyDialogProperties( name, ric, }, - }; + } as MatDialogConfig; + return data; } diff --git a/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.html b/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.html index e6d483f..29f0579 100644 --- a/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.html +++ b/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.html @@ -18,21 +18,21 @@ ========================LICENSE_END=================================== -->
- Number of instances: {{this.nbInstances()}} - -
- + -
+
@@ -41,14 +41,14 @@
- {{element.policy_id}} + {{instance.policy_id}} -
+
@@ -57,14 +57,14 @@
- {{element.ric_id}} + {{instance.ric_id}} -
+
@@ -73,14 +73,14 @@
- {{element.service_id}} + {{instance.service_id}} -
+
@@ -89,17 +89,17 @@
- {{toLocalTime(element.lastModified)}} + {{toLocalTime(instance.lastModified)}} Action - - @@ -113,4 +113,4 @@ -
\ No newline at end of file + \ No newline at end of file diff --git a/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.scss b/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.scss index 7f0ba47..c96ecfc 100644 --- a/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.scss +++ b/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.scss @@ -26,14 +26,42 @@ width: 100%; margin-top: 10px; margin-bottom: 10px; - background-color: grayscale($color: #eeeaea); + background-color: transparent; } .mat-column-instanceId { word-wrap: break-word; white-space: unset; - flex: 0 0 28%; - width: 28%; + flex: 0 0 22%; + width: 22%; +} + +.mat-column-ric { + word-wrap: break-word; + white-space: unset; + flex: 0 0 22%; + width: 22%; +} + +.mat-column-service { + word-wrap: break-word; + white-space: unset; + flex: 0 0 22%; + width: 22%; +} + +.mat-column-lastModified { + word-wrap: break-word; + white-space: unset; + flex: 0 0 22%; + width: 22%; +} + +.mat-column-action { + word-wrap: break-word; + white-space: unset; + flex: 0 0 12%; + width: 12%; } diff --git a/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.spec.ts b/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.spec.ts index 1260464..8f29d42 100644 --- a/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.spec.ts +++ b/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.spec.ts @@ -18,9 +18,22 @@ * ========================LICENSE_END=================================== */ -import { Component, ViewChild } from "@angular/core"; -import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { HarnessLoader } from "@angular/cdk/testing"; +import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed"; +import { HttpResponse } from "@angular/common/http"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { MatButtonHarness } from "@angular/material/button/testing"; import { MatDialog } from "@angular/material/dialog"; +import { MatIconModule } from "@angular/material/icon"; +import { MatInputHarness } from "@angular/material/input/testing"; +import { MatSortModule } from "@angular/material/sort"; +import { MatSortHarness } from "@angular/material/sort/testing"; +import { MatTableModule } from "@angular/material/table"; +import { MatTableHarness } from "@angular/material/table/testing"; +import { By } from "@angular/platform-browser"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { PolicyInstance, PolicyInstances, @@ -29,18 +42,59 @@ import { } from "@app/interfaces/policy.types"; import { PolicyService } from "@app/services/policy/policy.service"; import { ConfirmDialogService } from "@app/services/ui/confirm-dialog.service"; -import { ErrorDialogService } from "@app/services/ui/error-dialog.service"; import { NotificationService } from "@app/services/ui/notification.service"; import { UiService } from "@app/services/ui/ui.service"; import { ToastrModule } from "ngx-toastr"; -import { of } from "rxjs"; +import { Observable, of } from "rxjs"; +import { PolicyInstanceDialogComponent } from "../policy-instance-dialog/policy-instance-dialog.component"; import { PolicyInstanceComponent } from "./policy-instance.component"; +const lastModifiedTime = "2021-01-26T13:15:11.895297Z"; describe("PolicyInstanceComponent", () => { let hostComponent: PolicyInstanceComponentHostComponent; + let componentUnderTest: PolicyInstanceComponent; let hostFixture: ComponentFixture; + let loader: HarnessLoader; let policyServiceSpy: jasmine.SpyObj; let dialogSpy: jasmine.SpyObj; + let notificationServiceSpy: jasmine.SpyObj; + let confirmServiceSpy: jasmine.SpyObj; + + const policyInstances = { + policy_ids: ["policy1", "policy2"], + } as PolicyInstances; + const policyTypeSchema = JSON.parse( + '{"title": "1", "description": "Type 1 policy type"}' + ); + const policy1 = { + policy_id: "policy1", + policy_data: "{}", + ric_id: "1", + service_id: "service", + lastModified: "Now", + } as PolicyInstance; + const policy2 = { + policy_id: "policy2", + policy_data: "{}", + ric_id: "2", + service_id: "service", + lastModified: "Now", + } as PolicyInstance; + const policy1Status = { + last_modified: lastModifiedTime, + } as PolicyStatus; + const policy2Status = { + last_modified: lastModifiedTime, + } as PolicyStatus; + + const policyIdToInstanceMap = { + policy1: policy1, + policy2: policy2, + }; + const policyIdToStatusMap = { + policy1: policy1Status, + policy2: policy2Status, + }; @Component({ selector: "policy-instance-compnent-host-component", @@ -48,57 +102,52 @@ describe("PolicyInstanceComponent", () => { "", }) class PolicyInstanceComponentHostComponent { - @ViewChild(PolicyInstanceComponent) - componentUnderTest: PolicyInstanceComponent; - policyTypeSchema = JSON.parse( - '{"title": "1", "description": "Type 1 policy type"}' - ); policyType = { id: "type1", name: "1", - schemaObject: this.policyTypeSchema, + schemaObject: policyTypeSchema, } as PolicyTypeSchema; } - beforeEach(async(() => { + beforeEach(async () => { policyServiceSpy = jasmine.createSpyObj("PolicyService", [ "getPolicyInstancesByType", "getPolicyInstance", "getPolicyStatus", + "deletePolicy", ]); - let policyInstances = { policy_ids: ["policy1", "policy2"] } as PolicyInstances; policyServiceSpy.getPolicyInstancesByType.and.returnValue( of(policyInstances) ); - let policy1 = { - policy_id: "policy1", - policy_data: "{}", - ric_id: "1", - service_id: "service", - lastModified: "Now", - } as PolicyInstance; - let policy2 = { - policy_id: "policy2", - policy_data: "{}", - ric_id: "2", - service_id: "service", - lastModified: "Now", - } as PolicyInstance; - policyServiceSpy.getPolicyInstance.and.returnValues( - of(policy1), - of(policy2) - ); - let policy1Status = { last_modified: "Just now" } as PolicyStatus; - let policy2Status = { last_modified: "Before" } as PolicyStatus; - policyServiceSpy.getPolicyStatus.and.returnValues( - of(policy1Status), - of(policy2Status) - ); + policyServiceSpy.getPolicyInstance.and.callFake(function ( + policyId: string + ) { + return of(policyIdToInstanceMap[policyId]); + }); + policyServiceSpy.getPolicyStatus.and.callFake(function (policyId: string) { + return of(policyIdToStatusMap[policyId]); + }); dialogSpy = jasmine.createSpyObj("MatDialog", ["open"]); + notificationServiceSpy = jasmine.createSpyObj("NotificationService", [ + "success", + "warn", + ]); + confirmServiceSpy = jasmine.createSpyObj("ConfirmDialogService", [ + "openConfirmDialog", + ]); - TestBed.configureTestingModule({ - imports: [ToastrModule.forRoot()], + await TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + FormsModule, + MatIconModule, + MatSortModule, + MatTableModule, + ReactiveFormsModule, + ToastrModule.forRoot(), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], declarations: [ PolicyInstanceComponent, PolicyInstanceComponentHostComponent, @@ -106,21 +155,438 @@ describe("PolicyInstanceComponent", () => { providers: [ { provide: PolicyService, useValue: policyServiceSpy }, { provide: MatDialog, useValue: dialogSpy }, - ErrorDialogService, - NotificationService, - ConfirmDialogService, + { provide: NotificationService, useValue: notificationServiceSpy }, + { provide: ConfirmDialogService, useValue: confirmServiceSpy }, UiService, ], }).compileComponents(); - })); - beforeEach(() => { hostFixture = TestBed.createComponent(PolicyInstanceComponentHostComponent); hostComponent = hostFixture.componentInstance; + componentUnderTest = hostFixture.debugElement.query( + By.directive(PolicyInstanceComponent) + ).componentInstance; hostFixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(hostFixture); }); it("should create", () => { expect(hostComponent).toBeTruthy(); + + expect(componentUnderTest).toBeTruthy(); + }); + + it("should set correct dark mode from UIService", () => { + const uiService: UiService = TestBed.inject(UiService); + expect(componentUnderTest.darkMode).toBeTruthy(); + + uiService.darkModeState.next(false); + hostFixture.detectChanges(); + expect(componentUnderTest.darkMode).toBeFalsy(); + }); + + it("should contain number of instances heading and value, create and refresh buttons, and policies table", async () => { + const instancesHeading = hostFixture.debugElement.nativeElement.querySelector( + "div" + ); + expect(instancesHeading.innerText).toContain("Number of instances: 2"); + + const createButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#createButton" }) + ); + expect(createButton).toBeTruthy(); + const createIcon = hostFixture.debugElement.nativeElement.querySelector( + "#createIcon" + ); + expect(createIcon.innerText).toContain("add_box"); + + const refreshButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#refreshButton" }) + ); + expect(refreshButton).toBeTruthy(); + const refreshIcon = hostFixture.debugElement.nativeElement.querySelector( + "#refreshIcon" + ); + expect(refreshIcon.innerText).toContain("refresh"); + + const policiesTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + expect(policiesTable).toBeTruthy(); + }); + + it("should open dialog to create policy and refresh policies after successful creation", async () => { + const dialogRefSpy = setupDialogRefSpy(); + dialogSpy.open.and.returnValue(dialogRefSpy); + + spyOn(componentUnderTest, "getPolicyInstances"); + + const createButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#createButton" }) + ); + await createButton.click(); + + expect(dialogSpy.open).toHaveBeenCalledWith(PolicyInstanceDialogComponent, { + maxWidth: "1200px", + maxHeight: "900px", + width: "900px", + role: "dialog", + disableClose: false, + panelClass: "dark-theme", + data: { + createSchema: policyTypeSchema, + instanceId: null, + instanceJson: null, + name: "1", + ric: null, + }, + }); + expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled(); + }); + + it("should open dialog to edit policy and refresh policies after successful update", async () => { + const dialogRefSpy = setupDialogRefSpy(); + dialogSpy.open.and.returnValue(dialogRefSpy); + + spyOn(componentUnderTest, "getPolicyInstances"); + + const editButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#policy1EditButton" }) + ); + await editButton.click(); + + expect(dialogSpy.open).toHaveBeenCalledWith(PolicyInstanceDialogComponent, { + maxWidth: "1200px", + maxHeight: "900px", + width: "900px", + role: "dialog", + disableClose: false, + panelClass: "dark-theme", + data: { + createSchema: policyTypeSchema, + instanceId: "policy1", + instanceJson: "{}", + name: "1", + ric: "1", + }, + }); + expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled(); + }); + + it("should open dialog to edit policy and not refresh policies when dialog closed wihtout submit", async () => { + const dialogRefSpy = setupDialogRefSpy(false); + dialogSpy.open.and.returnValue(dialogRefSpy); + + spyOn(componentUnderTest, "getPolicyInstances"); + + const editButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#policy1EditButton" }) + ); + await editButton.click(); + + expect(componentUnderTest.getPolicyInstances).not.toHaveBeenCalled(); + }); + + it("should open instance dialog when clicking in any policy cell in table", async () => { + spyOn(componentUnderTest, "modifyInstance"); + + const policiesTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + const firstRow = (await policiesTable.getRows())[0]; + const idCell = (await firstRow.getCells())[0]; + (await idCell.host()).click(); + const ownerCell = (await firstRow.getCells())[1]; + (await ownerCell.host()).click(); + const serviceCell = (await firstRow.getCells())[2]; + (await serviceCell.host()).click(); + const lastModifiedCell = (await firstRow.getCells())[3]; + (await lastModifiedCell.host()).click(); + + // Totally unnecessary call just to make the bloody framework count the number of calls to the spy correctly! + await policiesTable.getRows(); + + expect(componentUnderTest.modifyInstance).toHaveBeenCalledTimes(4); + }); + + it("should open dialog asking for delete and delete when ok response and refresh table afterwards", async () => { + const dialogRefSpy = setupDialogRefSpy(); + confirmServiceSpy.openConfirmDialog.and.returnValue(dialogRefSpy); + const createResponse = { status: 204 } as HttpResponse; + policyServiceSpy.deletePolicy.and.returnValue(of(createResponse)); + + spyOn(componentUnderTest, "getPolicyInstances"); + const deleteButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#policy1DeleteButton" }) + ); + await deleteButton.click(); + + expect(confirmServiceSpy.openConfirmDialog).toHaveBeenCalledWith( + "Are you sure you want to delete this policy instance?" + ); + expect(policyServiceSpy.deletePolicy).toHaveBeenCalledWith("policy1"); + expect(notificationServiceSpy.success).toHaveBeenCalledWith( + "Delete succeeded!" + ); + expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled(); + }); + + it("should open dialog asking for delete and not delete whith Cancel as response", async () => { + const dialogRefSpy = setupDialogRefSpy(false); + confirmServiceSpy.openConfirmDialog.and.returnValue(dialogRefSpy); + + const deleteButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#policy1DeleteButton" }) + ); + await deleteButton.click(); + + expect(policyServiceSpy.deletePolicy).not.toHaveBeenCalled(); + }); + + it("should refresh table", async () => { + spyOn(componentUnderTest, "getPolicyInstances"); + + const refreshButton: MatButtonHarness = await loader.getHarness( + MatButtonHarness.with({ selector: "#refreshButton" }) + ); + await refreshButton.click(); + + expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled(); + }); + + describe("#policiesTable", () => { + const expectedPolicy1Row = { + instanceId: "policy1", + ric: "1", + service: "service", + lastModified: toLocalTime(lastModifiedTime), + action: "editdelete", + }; + + it("should contain correct headings", async () => { + const policiesTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + const headerRow = (await policiesTable.getHeaderRows())[0]; + const headers = await headerRow.getCellTextByColumnName(); + + expect(headers).toEqual({ + instanceId: "Instance", + ric: "Target", + service: "Owner", + lastModified: "Last modified", + action: "Action", + }); + }); + + it("should contain data after initialization", async () => { + const expectedJobRows = [ + expectedPolicy1Row, + { + instanceId: "policy2", + ric: "2", + service: "service", + lastModified: toLocalTime(lastModifiedTime), + action: "editdelete", + }, + ]; + const policiesTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + const policyRows = await policiesTable.getRows(); + expect(policyRows.length).toEqual(2); + policyRows.forEach((row) => { + row.getCellTextByColumnName().then((values) => { + expect(expectedJobRows).toContain(jasmine.objectContaining(values)); + }); + }); + }); + + it("should have filtering for all four policy data headings", async () => { + const policiesTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + + const idFilterInput = await loader.getHarness( + MatInputHarness.with({ selector: "#policyInstanceIdFilter" }) + ); + await idFilterInput.setValue("1"); + const policyRows = await policiesTable.getRows(); + expect(policyRows.length).toEqual(1); + expect(await policyRows[0].getCellTextByColumnName()).toEqual( + expectedPolicy1Row + ); + + const targetFilterInput = await loader.getHarness( + MatInputHarness.with({ selector: "#policyInstanceTargetFilter" }) + ); + expect(targetFilterInput).toBeTruthy(); + + const ownerFilterInput = await loader.getHarness( + MatInputHarness.with({ selector: "#policyInstanceOwnerFilter" }) + ); + expect(ownerFilterInput).toBeTruthy(); + + const lastModifiedFilterInput = await loader.getHarness( + MatInputHarness.with({ selector: "#policyInstanceLastModifiedFilter" }) + ); + expect(lastModifiedFilterInput).toBeTruthy(); + }); + + it("should not sort when click in filter inputs", async () => { + spyOn(componentUnderTest, "stopSort").and.callThrough(); + + const idFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector( + "#idSortStop" + ); + idFilterInputDiv.click(); + + const targetFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector( + "#targetSortStop" + ); + targetFilterInputDiv.click(); + + const ownerFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector( + "#ownerSortStop" + ); + ownerFilterInputDiv.click(); + + const lastModifiedFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector( + "#lastModifiedSortStop" + ); + lastModifiedFilterInputDiv.click(); + + expect(componentUnderTest.stopSort).toHaveBeenCalledTimes(4); + + const eventSpy = jasmine.createSpyObj("any", ["stopPropagation"]); + componentUnderTest.stopSort(eventSpy); + expect(eventSpy.stopPropagation).toHaveBeenCalled(); + }); + + describe("#sorting", () => { + it("should verify sort functionality on the table", async () => { + const sort = await loader.getHarness(MatSortHarness); + const headers = await sort.getSortHeaders({ sortDirection: "" }); + expect(headers.length).toBe(4); + + await headers[0].click(); + expect(await headers[0].isActive()).toBe(true); + expect(await headers[0].getSortDirection()).toBe("asc"); + + await headers[0].click(); + expect(await headers[0].getSortDirection()).toBe("desc"); + }); + + it("should sort table asc and desc by first header", async () => { + const sort = await loader.getHarness(MatSortHarness); + const policyTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + const firstHeader = (await sort.getSortHeaders())[0]; + expect(await firstHeader.getSortDirection()).toBe(""); + + await firstHeader.click(); + expect(await firstHeader.getSortDirection()).toBe("asc"); + let policyRows = await policyTable.getRows(); + expect(await policyRows[0].getCellTextByColumnName()).toEqual( + expectedPolicy1Row + ); + + await firstHeader.click(); + expect(await firstHeader.getSortDirection()).toBe("desc"); + policyRows = await policyTable.getRows(); + expect( + await policyRows[policyRows.length - 1].getCellTextByColumnName() + ).toEqual(expectedPolicy1Row); + }); + + it("should sort table asc and desc by second header", async () => { + const sort = await loader.getHarness(MatSortHarness); + const jobsTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + const firstHeader = (await sort.getSortHeaders())[1]; + expect(await firstHeader.getSortDirection()).toBe(""); + + await firstHeader.click(); + expect(await firstHeader.getSortDirection()).toBe("asc"); + let policyRows = await jobsTable.getRows(); + policyRows = await jobsTable.getRows(); + expect(await policyRows[0].getCellTextByColumnName()).toEqual( + expectedPolicy1Row + ); + + await firstHeader.click(); + expect(await firstHeader.getSortDirection()).toBe("desc"); + policyRows = await jobsTable.getRows(); + expect( + await policyRows[policyRows.length - 1].getCellTextByColumnName() + ).toEqual(expectedPolicy1Row); + }); + + it("should sort table asc and desc by third header", async () => { + const sort = await loader.getHarness(MatSortHarness); + const jobsTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + const firstHeader = (await sort.getSortHeaders())[2]; + expect(await firstHeader.getSortDirection()).toBe(""); + + await firstHeader.click(); + expect(await firstHeader.getSortDirection()).toBe("asc"); + let policyRows = await jobsTable.getRows(); + policyRows = await jobsTable.getRows(); + expect(await policyRows[0].getCellTextByColumnName()).toEqual( + expectedPolicy1Row + ); + + await firstHeader.click(); + expect(await firstHeader.getSortDirection()).toBe("desc"); + policyRows = await jobsTable.getRows(); + expect( + await policyRows[policyRows.length - 1].getCellTextByColumnName() + ).toEqual(expectedPolicy1Row); + }); + + it("should sort table asc and desc by fourth header", async () => { + const sort = await loader.getHarness(MatSortHarness); + const jobsTable = await loader.getHarness( + MatTableHarness.with({ selector: "#policiesTable" }) + ); + const firstHeader = (await sort.getSortHeaders())[3]; + expect(await firstHeader.getSortDirection()).toBe(""); + + await firstHeader.click(); + expect(await firstHeader.getSortDirection()).toBe("asc"); + let policyRows = await jobsTable.getRows(); + policyRows = await jobsTable.getRows(); + expect(await policyRows[0].getCellTextByColumnName()).toEqual( + expectedPolicy1Row + ); + + await firstHeader.click(); + expect(await firstHeader.getSortDirection()).toBe("desc"); + policyRows = await jobsTable.getRows(); + expect( + await policyRows[policyRows.length - 1].getCellTextByColumnName() + ).toEqual(expectedPolicy1Row); + }); + }); }); }); + +function setupDialogRefSpy(returnValue: boolean = true) { + const afterClosedObservable = new Observable((observer) => { + observer.next(returnValue); + }); + + const dialogRefSpy = jasmine.createSpyObj("MatDialogRef", ["afterClosed"]); + dialogRefSpy.afterClosed.and.returnValue(afterClosedObservable); + return dialogRefSpy; +} + +function toLocalTime(utcTime: string): string { + const date = new Date(utcTime); + const toutc = date.toUTCString(); + return new Date(toutc + " UTC").toLocaleString(); +} diff --git a/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.ts b/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.ts index aafc08e..6441e56 100644 --- a/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.ts +++ b/webapp-frontend/src/app/policy/policy-instance/policy-instance.component.ts @@ -22,24 +22,18 @@ import { Sort } from "@angular/material/sort"; import { Component, OnInit, Input } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; import { PolicyTypeSchema } from "@interfaces/policy.types"; -import { ErrorDialogService } from "@services/ui/error-dialog.service"; import { NotificationService } from "@services/ui/notification.service"; import { PolicyService } from "@services/policy/policy.service"; import { ConfirmDialogService } from "@services/ui/confirm-dialog.service"; import { PolicyInstance } from "@interfaces/policy.types"; import { PolicyInstanceDialogComponent } from "../policy-instance-dialog/policy-instance-dialog.component"; import { getPolicyDialogProperties } from "../policy-instance-dialog/policy-instance-dialog.component"; -import { HttpErrorResponse, HttpResponse } from "@angular/common/http"; -import { BehaviorSubject } from "rxjs"; +import { HttpResponse } from "@angular/common/http"; +import { BehaviorSubject, forkJoin } from "rxjs"; import { UiService } from "@services/ui/ui.service"; import { FormControl, FormGroup } from "@angular/forms"; import { MatTableDataSource } from "@angular/material/table"; - -class PolicyTypeInfo { - constructor(public type: PolicyTypeSchema) {} - - isExpanded: BehaviorSubject = new BehaviorSubject(false); -} +import { mergeMap } from "rxjs/operators"; @Component({ selector: "nrcp-policy-instance", @@ -48,17 +42,15 @@ class PolicyTypeInfo { }) export class PolicyInstanceComponent implements OnInit { @Input() policyTypeSchema: PolicyTypeSchema; - policyInstances: PolicyInstance[] = []; - private policyInstanceSubject = new BehaviorSubject([]); - policyTypeInfo = new Map(); - instanceDataSource: MatTableDataSource = new MatTableDataSource(); - policyInstanceForm: FormGroup; darkMode: boolean; + instanceDataSource: MatTableDataSource; + policyInstanceForm: FormGroup; + private policyInstanceSubject = new BehaviorSubject([]); + policyInstances: PolicyInstance[] = []; constructor( private policySvc: PolicyService, private dialog: MatDialog, - private errorDialogService: ErrorDialogService, private notificationService: NotificationService, private confirmDialogService: ConfirmDialogService, private ui: UiService @@ -74,26 +66,29 @@ export class PolicyInstanceComponent implements OnInit { ngOnInit() { this.getPolicyInstances(); this.policyInstanceSubject.subscribe((data) => { - this.instanceDataSource.data = data; + this.instanceDataSource = new MatTableDataSource(data); + + this.instanceDataSource.filterPredicate = (( + data: PolicyInstance, + filter + ) => { + return ( + this.isDataIncluding(data.policy_id, filter.id) && + this.isDataIncluding(data.ric_id, filter.target) && + this.isDataIncluding(data.service_id, filter.owner) && + this.isDataIncluding(data.lastModified, filter.lastModified) + ); + }) as (data: PolicyInstance, filter: any) => boolean; }); this.policyInstanceForm.valueChanges.subscribe((value) => { - const filter = { ...value, id: value.id.trim().toLowerCase() } as string; + const filter = { + ...value, + id: value.id.trim().toLowerCase(), + } as string; this.instanceDataSource.filter = filter; }); - this.instanceDataSource.filterPredicate = (( - data: PolicyInstance, - filter - ) => { - return ( - this.isDataIncluding(data.policy_id, filter.id) && - this.isDataIncluding(data.ric_id, filter.target) && - this.isDataIncluding(data.service_id, filter.owner) && - this.isDataIncluding(data.lastModified, filter.lastModified) - ); - }) as (data: PolicyInstance, filter: any) => boolean; - this.ui.darkModeState.subscribe((isDark) => { this.darkMode = isDark; }); @@ -102,29 +97,33 @@ export class PolicyInstanceComponent implements OnInit { getPolicyInstances() { this.policyInstances = [] as PolicyInstance[]; this.policySvc - .getPolicyInstancesByType(this.policyTypeSchema.id) - .subscribe((policies) => { - if (policies.policy_ids.length != 0) { - policies.policy_ids.forEach((policyId) => { - this.policySvc - .getPolicyInstance(policyId) - .subscribe((policyInstance) => { - this.policySvc - .getPolicyStatus(policyId) - .subscribe((policyStatus) => { - policyInstance.lastModified = policyStatus.last_modified; - }); - this.policyInstances.push(policyInstance); - }); - this.policyInstanceSubject.next(this.policyInstances); - }); - } + .getPolicyInstancesByType(this.policyTypeSchema.id) + .pipe( + mergeMap((policyIds) => + forkJoin( + policyIds.policy_ids.map((id) => { + return forkJoin([ + this.policySvc.getPolicyInstance(id), + this.policySvc.getPolicyStatus(id), + ]); + }) + ) + ) + ) + .subscribe((res) => { + this.policyInstances = res.map((policy) => { + let policyInstance = {}; + policyInstance = policy[0]; + policyInstance.lastModified = policy[1].last_modified; + return policyInstance; + }); + this.policyInstanceSubject.next(this.policyInstances); }); } getSortedData(sort: Sort) { const data = this.instanceDataSource.data; - data.sort((a, b) => { + data.sort((a: PolicyInstance, b: PolicyInstance) => { const isAsc = sort.direction === "asc"; switch (sort.active) { case "instanceId": @@ -150,43 +149,37 @@ export class PolicyInstanceComponent implements OnInit { return !filter || data.toLowerCase().includes(filter); } - private onExpand(isExpanded: boolean) { - if (isExpanded) { - this.getPolicyInstances(); - } + createPolicyInstance(policyTypeSchema: PolicyTypeSchema): void { + this.openInstanceDialog(null); } - private isSchemaEmpty(): boolean { - return this.policyTypeSchema.schemaObject === "{}"; + modifyInstance(instance: PolicyInstance): void { + let refreshedInstance: PolicyInstance; + this.policySvc + .getPolicyInstance(instance.policy_id) + .subscribe((refreshedJson: any) => { + refreshedInstance = refreshedJson; + }); + + this.openInstanceDialog(refreshedInstance); } - modifyInstance(instance: PolicyInstance): void { - this.policySvc.getPolicyInstance(instance.policy_id).subscribe( - (refreshedJson: any) => { - instance = refreshedJson; - this.dialog - .open( - PolicyInstanceDialogComponent, - getPolicyDialogProperties( - this.policyTypeSchema, - instance, - this.darkMode - ) - ) - .afterClosed() - .subscribe((_: any) => { - this.getPolicyInstances(); - }); - }, - (httpError: HttpErrorResponse) => { - this.notificationService.error( - "Could not refresh instance. Please try again." + httpError.message - ); - } + private openInstanceDialog(policy: PolicyInstance) { + const dialogData = getPolicyDialogProperties( + this.policyTypeSchema, + policy, + this.darkMode + ); + const dialogRef = this.dialog.open( + PolicyInstanceDialogComponent, + dialogData ); + dialogRef.afterClosed().subscribe((ok: any) => { + if (ok) this.getPolicyInstances(); + }); } - nbInstances(): number { + noInstances(): number { return this.policyInstances.length; } @@ -196,17 +189,6 @@ export class PolicyInstanceComponent implements OnInit { return new Date(toutc + " UTC").toLocaleString(); } - createPolicyInstance(policyTypeSchema: PolicyTypeSchema): void { - let dialogRef = this.dialog.open( - PolicyInstanceDialogComponent, - getPolicyDialogProperties(policyTypeSchema, null, this.darkMode) - ); - const info: PolicyTypeInfo = this.getPolicyTypeInfo(policyTypeSchema); - dialogRef.afterClosed().subscribe((_) => { - info.isExpanded.next(info.isExpanded.getValue()); - }); - } - deleteInstance(instance: PolicyInstance): void { this.confirmDialogService .openConfirmDialog( @@ -215,38 +197,18 @@ export class PolicyInstanceComponent implements OnInit { .afterClosed() .subscribe((res: any) => { if (res) { - this.policySvc.deletePolicy(instance.policy_id).subscribe( - (response: HttpResponse) => { - switch (response.status) { - case 204: - this.notificationService.success("Delete succeeded!"); - this.getPolicyInstances(); - break; - default: - this.notificationService.warn( - "Delete failed " + response.status + " " + response.body - ); + this.policySvc + .deletePolicy(instance.policy_id) + .subscribe((response: HttpResponse) => { + if (response.status === 204) { + this.notificationService.success("Delete succeeded!"); + this.getPolicyInstances(); } - }, - (error: HttpErrorResponse) => { - this.errorDialogService.displayError( - error.statusText + ", " + error.error - ); - } - ); + }); } }); } - getPolicyTypeInfo(policyTypeSchema: PolicyTypeSchema): PolicyTypeInfo { - let info: PolicyTypeInfo = this.policyTypeInfo.get(policyTypeSchema.name); - if (!info) { - info = new PolicyTypeInfo(policyTypeSchema); - this.policyTypeInfo.set(policyTypeSchema.name, info); - } - return info; - } - refreshTable() { this.getPolicyInstances(); } diff --git a/webapp-frontend/src/app/policy/ric-selector/ric-selector.component.ts b/webapp-frontend/src/app/policy/ric-selector/ric-selector.component.ts index 6b502c6..fec3d8a 100644 --- a/webapp-frontend/src/app/policy/ric-selector/ric-selector.component.ts +++ b/webapp-frontend/src/app/policy/ric-selector/ric-selector.component.ts @@ -66,14 +66,12 @@ export class RicSelectorComponent implements OnInit { private fetchRics() { if (!this.policyTypeName) this.policyTypeName = ""; - console.log("fetchRics ", this.policyTypeName); const self: RicSelectorComponent = this; this.dataService.getRics(this.policyTypeName).subscribe({ next(value: Rics) { value.rics.forEach((ric) => { self.allRics.push(ric.ric_id); }); - console.log(value); }, }); } -- 2.16.6