Skip to content

Commit bd179b4

Browse files
authored
WFPREV-862 : Add validation API and add call to frontend (#1120)
1 parent 76c3080 commit bd179b4

File tree

15 files changed

+563
-66
lines changed

15 files changed

+563
-66
lines changed

client/wfprev-war/src/main/angular/src/app/components/add-attachment/add-attachment.component.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2-
import { AddAttachmentComponent } from './add-attachment.component';
3-
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
2+
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
43
import { TokenService } from 'src/app/services/token.service';
4+
import { AddAttachmentComponent } from './add-attachment.component';
55

66
describe('AddAttachmentComponent', () => {
77
let component: AddAttachmentComponent;
@@ -157,11 +157,11 @@ describe('AddAttachmentComponent', () => {
157157
{ provide: MAT_DIALOG_DATA, useValue: { indicator: 'activity-files', name: 'Activity' } },
158158
],
159159
}).compileComponents();
160-
160+
161161
const newFixture = TestBed.createComponent(AddAttachmentComponent);
162162
const newComponent = newFixture.componentInstance;
163163
newFixture.detectChanges();
164-
164+
165165
expect(newComponent.attachmentTypes).toEqual([
166166
{ label: 'Activity Polygon', value: 'MAP' },
167167
{ label: 'Other', value: 'OTHER' },
@@ -182,12 +182,12 @@ describe('AddAttachmentComponent', () => {
182182
component.selectedFileName = 'file.txt';
183183
component.attachmentType = 'MAP';
184184
component.description = 'a'.repeat(151); // invalid length of text
185-
185+
186186
component.onConfirm();
187-
187+
188188
expect(mockDialogRef.close).not.toHaveBeenCalled();
189189
});
190-
191-
192-
190+
191+
192+
193193
});

client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-files/project-files.component.spec.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,18 @@ describe('ProjectFilesComponent', () => {
5252

5353
// Setup default mock return values to prevent subscribe errors
5454
mockAttachmentService.getProjectAttachments.and.returnValue(of({ _embedded: {} }));
55+
mockAttachmentService.getActivityAttachments.and.returnValue(of({ _embedded: {} }));
5556
mockProjectService.getProjectBoundaries.and.returnValue(of({ _embedded: {} }));
57+
mockProjectService.getActivityBoundaries.and.returnValue(of({ _embedded: {} }));
5658
mockSpatialService.extractCoordinates.and.returnValue(Promise.resolve([]));
5759
mockAttachmentService.createProjectAttachment.and.returnValue(of({}));
60+
mockAttachmentService.createActivityAttachment.and.returnValue(of({}));
5861
mockAttachmentService.deleteProjectAttachment.and.returnValue(of({}));
62+
mockAttachmentService.deleteActivityAttachments.and.returnValue(of({}));
5963
mockProjectService.createProjectBoundary.and.returnValue(of({}));
64+
mockProjectService.createActivityBoundary.and.returnValue(of({}));
65+
mockProjectService.deleteProjectBoundary.and.returnValue(of({}));
66+
mockProjectService.deleteActivityBoundary.and.returnValue(of({}));
6067
mockProjectService.uploadDocument.and.returnValue(of({}));
6168
mockSnackRef = { dismiss: jasmine.createSpy('dismiss') } as any;
6269
mockSnackbar.open.and.returnValue(mockSnackRef);
@@ -592,7 +599,9 @@ describe('ProjectFilesComponent', () => {
592599
const mockProjectGuid = 'mock-guid';
593600
const mockProjectFile: ProjectFile = {
594601
fileAttachmentGuid: 'test-guid',
595-
fileName: 'test-file.txt'
602+
fileName: 'test-file.txt',
603+
attachmentContentTypeCode: { attachmentContentTypeCode: 'MAP' },
604+
sourceObjectUniqueId: 'boundary-guid'
596605
};
597606

598607
const mockBoundary = {
@@ -639,7 +648,6 @@ describe('ProjectFilesComponent', () => {
639648
'test-guid'
640649
);
641650

642-
expect(mockProjectService.getProjectBoundaries).toHaveBeenCalledWith(mockProjectGuid);
643651
expect(mockProjectService.deleteProjectBoundary).toHaveBeenCalledWith(mockProjectGuid, 'boundary-guid');
644652

645653
expect(component.projectFiles.length).toBe(0);
@@ -896,7 +904,11 @@ describe('ProjectFilesComponent', () => {
896904
component.fiscalGuid = 'fiscal-guid';
897905
component.projectGuid = 'project-guid';
898906

899-
const mockFile = { fileAttachmentGuid: 'test-guid', attachmentContentTypeCode: { attachmentContentTypeCode: 'MAP' } } as ProjectFile;
907+
const mockFile = {
908+
fileAttachmentGuid: 'test-guid',
909+
attachmentContentTypeCode: { attachmentContentTypeCode: 'MAP' },
910+
sourceObjectUniqueId: 'boundary-guid'
911+
} as ProjectFile;
900912
const mockBoundary = {
901913
activityBoundaryGuid: 'boundary-guid',
902914
systemStartTimestamp: new Date().toISOString()
@@ -921,7 +933,6 @@ describe('ProjectFilesComponent', () => {
921933
expect(mockAttachmentService.deleteActivityAttachments).toHaveBeenCalledWith(
922934
'project-guid', 'fiscal-guid', 'activity-guid', 'test-guid'
923935
);
924-
expect(mockProjectService.getActivityBoundaries).toHaveBeenCalled();
925936
expect(mockProjectService.deleteActivityBoundary).toHaveBeenCalledWith(
926937
'project-guid', 'fiscal-guid', 'activity-guid', 'boundary-guid'
927938
);
@@ -965,21 +976,22 @@ describe('ProjectFilesComponent', () => {
965976
component.projectGuid = 'project-guid';
966977
const mockFile = {
967978
fileAttachmentGuid: 'test-guid',
968-
attachmentContentTypeCode: { attachmentContentTypeCode: 'MAP' }
979+
attachmentContentTypeCode: { attachmentContentTypeCode: 'MAP' },
980+
sourceObjectUniqueId: 'boundary-guid'
969981
} as ProjectFile;
970982

971983
mockDialog.open.and.returnValue({ afterClosed: () => of(true) } as any);
972-
mockAttachmentService.deleteActivityAttachments = jasmine.createSpy().and.returnValue(of({}));
973-
mockProjectService.getActivityBoundaries = jasmine.createSpy().and.returnValue(of({
984+
mockAttachmentService.deleteActivityAttachments.and.returnValue(of({}));
985+
mockProjectService.getActivityBoundaries.and.returnValue(of({
974986
_embedded: { activityBoundary: [] }
975987
}));
976988

977-
spyOn(console, 'log');
989+
spyOn(component, 'loadActivityAttachments').and.callThrough();
978990
component.deleteFile(mockFile);
979991

980992
expect(mockAttachmentService.deleteActivityAttachments).toHaveBeenCalled();
981993
expect(mockProjectService.getActivityBoundaries).toHaveBeenCalled();
982-
expect(console.log).toHaveBeenCalledWith('No boundaries found');
994+
// Implementation doesn't log "No boundaries found" anymore, removing expectation
983995
});
984996

985997
describe('finishWithoutGeometry', () => {

client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-files/project-files.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,13 @@ export class ProjectFilesComponent implements OnInit {
336336
}
337337
});
338338
}
339+
}).catch((error) => {
340+
snackRef.dismiss();
341+
console.error('Error extracting coordinates:', error);
342+
this.snackbarService.open('Failed to process spatial file. ' + error.message, 'Close', {
343+
duration: 10000,
344+
panelClass: ['snackbar-error'],
345+
});
339346
});
340347
}
341348

client/wfprev-war/src/main/angular/src/app/components/edit-project/wfprev-performance-update/wfprev-performance-updates.component.spec.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,51 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
22

3+
import { MatDialog } from '@angular/material/dialog';
4+
import { MatSnackBar } from '@angular/material/snack-bar';
5+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
6+
import { ActivatedRoute } from '@angular/router';
7+
import { CodeTableServices } from 'src/app/services/code-table-services';
8+
import { ProjectFiscalsSignalService } from 'src/app/services/project-fiscals-signal.service';
9+
import { ProjectService } from 'src/app/services/project-services';
310
import { PerformanceUpdatesComponent } from './wfprev-performance-updates.component';
411

512
describe('PerformanceUpdatesComponent', () => {
613
let component: PerformanceUpdatesComponent;
714
let fixture: ComponentFixture<PerformanceUpdatesComponent>;
15+
let mockProjectService: jasmine.SpyObj<ProjectService>;
16+
let mockCodeTableService: jasmine.SpyObj<CodeTableServices>;
17+
let mockProjectFiscalsSignalService: jasmine.SpyObj<ProjectFiscalsSignalService>;
18+
let mockDialog: jasmine.SpyObj<MatDialog>;
19+
let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
820

921
beforeEach(async () => {
22+
mockProjectService = jasmine.createSpyObj('ProjectService', ['getPerformanceUpdates', 'getProjectFiscalByProjectPlanFiscalGuid']);
23+
mockCodeTableService = jasmine.createSpyObj('CodeTableServices', ['fetchCodeTable']);
24+
mockProjectFiscalsSignalService = jasmine.createSpyObj('ProjectFiscalsSignalService', ['trigger']);
25+
mockDialog = jasmine.createSpyObj('MatDialog', ['open']);
26+
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['open']);
27+
1028
await TestBed.configureTestingModule({
11-
imports: [PerformanceUpdatesComponent]
29+
imports: [PerformanceUpdatesComponent, BrowserAnimationsModule],
30+
providers: [
31+
{ provide: ProjectService, useValue: mockProjectService },
32+
{ provide: CodeTableServices, useValue: mockCodeTableService },
33+
{ provide: ProjectFiscalsSignalService, useValue: mockProjectFiscalsSignalService },
34+
{ provide: MatDialog, useValue: mockDialog },
35+
{ provide: MatSnackBar, useValue: mockSnackBar },
36+
{
37+
provide: ActivatedRoute,
38+
useValue: {
39+
snapshot: {
40+
queryParamMap: {
41+
get: () => 'test-project-guid'
42+
}
43+
}
44+
}
45+
}
46+
]
1247
})
13-
.compileComponents();
48+
.compileComponents();
1449

1550
fixture = TestBed.createComponent(PerformanceUpdatesComponent);
1651
component = fixture.componentInstance;

client/wfprev-war/src/main/angular/src/app/components/search-filter/search-filter.component.spec.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
2-
import { SearchFilterComponent } from './search-filter.component';
3-
import { SharedCodeTableService } from 'src/app/services/shared-code-table.service';
1+
import { signal } from '@angular/core';
2+
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
3+
import { MatOptionSelectionChange } from '@angular/material/core';
4+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
5+
import { ActivatedRoute } from '@angular/router';
6+
import { Subject } from 'rxjs';
47
import { CodeTableServices } from 'src/app/services/code-table-services';
8+
import { ProjectFilterStateService } from 'src/app/services/project-filter-state.service';
9+
import { SharedCodeTableService } from 'src/app/services/shared-code-table.service';
510
import { SharedService } from 'src/app/services/shared-service';
6-
import { of, Subject } from 'rxjs';
7-
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
8-
import { MatOptionSelectionChange } from '@angular/material/core';
11+
import { SearchFilterComponent } from './search-filter.component';
912

1013
describe('SearchFilterComponent', () => {
1114
let component: SearchFilterComponent;
@@ -14,6 +17,7 @@ describe('SearchFilterComponent', () => {
1417
let mockCodeTableService: jasmine.SpyObj<CodeTableServices>;
1518
let mockSharedCodeTableService: jasmine.SpyObj<SharedCodeTableService>;
1619
let mockCodeTablesSubject: Subject<any>;
20+
let mockProjectFilterStateService: jasmine.SpyObj<ProjectFilterStateService>;
1721

1822
beforeEach(async () => {
1923
mockSharedService = jasmine.createSpyObj('SharedService', ['updateFilters']);
@@ -27,12 +31,24 @@ describe('SearchFilterComponent', () => {
2731
}
2832
} as any;
2933

34+
mockProjectFilterStateService = jasmine.createSpyObj('ProjectFilterStateService', ['update']);
35+
(mockProjectFilterStateService as any).filters = signal({});
36+
3037
await TestBed.configureTestingModule({
3138
imports: [SearchFilterComponent, BrowserAnimationsModule],
3239
providers: [
3340
{ provide: SharedService, useValue: mockSharedService },
3441
{ provide: CodeTableServices, useValue: mockCodeTableService },
35-
{ provide: SharedCodeTableService, useValue: mockSharedCodeTableService }
42+
{ provide: SharedCodeTableService, useValue: mockSharedCodeTableService },
43+
{ provide: ProjectFilterStateService, useValue: mockProjectFilterStateService },
44+
{
45+
provide: ActivatedRoute,
46+
useValue: {
47+
snapshot: {
48+
url: []
49+
}
50+
}
51+
}
3652
]
3753
}).compileComponents();
3854

@@ -80,7 +96,7 @@ describe('SearchFilterComponent', () => {
8096

8197
expect(mockSharedService.updateFilters).toHaveBeenCalledWith(
8298
jasmine.objectContaining({
83-
fiscalYear: ['2025', '2024', '2023', 'null']
99+
fiscalYears: ['2025', '2024', '2023', 'null']
84100
})
85101
);
86102
});
@@ -259,7 +275,7 @@ describe('SearchFilterComponent', () => {
259275

260276
spyOn(component, 'emitFilters');
261277

262-
component.assignDefaultFiscalYear();
278+
component.assignDefaultFiscalYear();
263279

264280
expect(component.selectedFiscalYears).toContain(fyValue);
265281
expect(component.selectedFiscalYears).toContain('null');

client/wfprev-war/src/main/angular/src/app/components/shared/textarea/textarea.component.spec.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2-
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
2+
import { FormControl, Validators } from '@angular/forms';
33
import { By } from '@angular/platform-browser';
44
import { TextareaComponent } from './textarea.component';
55

@@ -49,13 +49,17 @@ describe('TextareaComponent', () => {
4949
});
5050

5151
it('should show error when maxlength exceeded and touched', () => {
52+
component.maxLength = 5;
5253
component.control = new FormControl('too long text', [Validators.maxLength(5)]);
5354
component.control.markAsTouched();
5455
fixture.detectChanges();
5556

56-
const errorEl = fixture.debugElement.query(By.css('.error'));
57-
expect(errorEl).toBeTruthy();
58-
expect(errorEl.nativeElement.textContent).toContain('Maximum character');
57+
const errors = fixture.debugElement.queryAll(By.css('.error'));
58+
const maxLengthError = errors.find(el => el.nativeElement.textContent.includes('Maximum character'));
59+
expect(maxLengthError).toBeTruthy();
60+
if (maxLengthError) {
61+
expect(maxLengthError.nativeElement.classList.contains('invisible')).toBeFalse();
62+
}
5963
});
6064

6165
it('should not show error when control is untouched', () => {

client/wfprev-war/src/main/angular/src/app/components/wfprev-performance-update-modal-window/wfprev-performance-update-modal-window.component.spec.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
22

3+
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
4+
import { MatSnackBar } from '@angular/material/snack-bar';
5+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
6+
import { ProjectService } from 'src/app/services/project-services';
37
import { PerformanceUpdateModalWindowComponent } from './wfprev-performance-update-modal-window.component';
48

59
describe('PerformanceUpdateModalWindowComponent', () => {
610
let component: PerformanceUpdateModalWindowComponent;
711
let fixture: ComponentFixture<PerformanceUpdateModalWindowComponent>;
12+
let mockDialogRef: jasmine.SpyObj<MatDialogRef<PerformanceUpdateModalWindowComponent>>;
13+
let mockProjectService: jasmine.SpyObj<ProjectService>;
14+
let mockSnackbar: jasmine.SpyObj<MatSnackBar>;
15+
let mockDialog: jasmine.SpyObj<MatDialog>;
816

917
beforeEach(async () => {
18+
mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['close']);
19+
mockProjectService = jasmine.createSpyObj('ProjectService', ['savePerformanceUpdates']);
20+
mockSnackbar = jasmine.createSpyObj('MatSnackBar', ['open']);
21+
mockDialog = jasmine.createSpyObj('MatDialog', ['open']);
22+
1023
await TestBed.configureTestingModule({
11-
imports: [PerformanceUpdateModalWindowComponent]
24+
imports: [PerformanceUpdateModalWindowComponent, BrowserAnimationsModule],
25+
providers: [
26+
{ provide: MatDialogRef, useValue: mockDialogRef },
27+
{ provide: ProjectService, useValue: mockProjectService },
28+
{ provide: MatSnackBar, useValue: mockSnackbar },
29+
{ provide: MatDialog, useValue: mockDialog },
30+
{
31+
provide: MAT_DIALOG_DATA,
32+
useValue: {
33+
currentForecast: 100,
34+
reportingPeriod: [],
35+
progressStatus: [],
36+
projectGuid: 'test-project',
37+
fiscalGuid: 'test-fiscal'
38+
}
39+
}
40+
]
1241
})
13-
.compileComponents();
42+
.compileComponents();
1443

1544
fixture = TestBed.createComponent(PerformanceUpdateModalWindowComponent);
1645
component = fixture.componentInstance;

client/wfprev-war/src/main/angular/src/app/services/spatial-services.spec.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { TestBed } from '@angular/core/testing';
21
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
3-
import { SpatialService } from './spatial-services';
4-
import { Geometry, Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, Position } from 'geojson';
5-
import { of, throwError } from 'rxjs';
6-
import * as turf from '@turf/turf';
2+
import { TestBed } from '@angular/core/testing';
3+
import { MatSnackBar } from '@angular/material/snack-bar';
74
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
85
import { ZipReader } from '@zip.js/zip.js';
9-
import { MatSnackBar } from '@angular/material/snack-bar';
6+
import { Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Position } from 'geojson';
7+
import { of, throwError } from 'rxjs';
108
import { AppConfigService } from './app-config.service';
9+
import { SpatialService } from './spatial-services';
1110
import { TokenService } from './token.service';
1211

1312
// Create mock ZIP module implementation
@@ -657,14 +656,14 @@ describe('SpatialService', () => {
657656
it('should extract and process geometry when the API returns valid data', (done: () => void) => {
658657
// Spy on stripAltitude and return the input geometry unchanged
659658
const stripAltitudeSpy = spyOn(service, 'stripAltitude').and.callFake((geom) => geom);
660-
659+
661660
service.extractGDBGeometry(mockFile).subscribe((result) => {
662-
expect(result.length).toBe(1);
661+
expect(result.length).toBe(1);
663662
expect(stripAltitudeSpy).toHaveBeenCalledTimes(1);
664663
expect(stripAltitudeSpy.calls.mostRecent().args[0]).toEqual(mockGeometry);
665664
done();
666665
});
667-
666+
668667
const req = httpMock.expectOne('http://mock-api.com/wfprev-api/gdb/extract');
669668
expect(req.request.method).toBe('POST');
670669
req.flush({ body: JSON.stringify([mockGeometry]) });
@@ -826,6 +825,8 @@ describe('SpatialService', () => {
826825
mockAppConfigService as any,
827826
mockTokenService as any
828827
);
828+
829+
spyOn(service, 'validateGeometryWithBackend').and.returnValue(Promise.resolve({ valid: true, message: 'Valid' }));
829830
});
830831
it('should validate a correct multipolygon without errors', async () => {
831832
const coords: Position[][][] = [

0 commit comments

Comments
 (0)