diff --git a/package-lock.json b/package-lock.json index adf1819..5ce714c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,14 +23,14 @@ "@ngneat/error-tailor": "^5.0.1", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "@onecx/accelerator": "^5.4.0", - "@onecx/angular-accelerator": "^5.4.0", - "@onecx/angular-auth": "^5.4.0", - "@onecx/angular-webcomponents": "^5.4.0", - "@onecx/integration-interface": "^5.4.0", - "@onecx/keycloak-auth": "^5.4.0", - "@onecx/portal-integration-angular": "^5.4.0", - "@onecx/portal-layout-styles": "^5.4.0", + "@onecx/accelerator": "^5.8.0", + "@onecx/angular-accelerator": "^5.8.0", + "@onecx/angular-auth": "^5.8.0", + "@onecx/angular-webcomponents": "^5.8.0", + "@onecx/integration-interface": "^5.8.0", + "@onecx/keycloak-auth": "^5.8.0", + "@onecx/portal-integration-angular": "^5.8.0", + "@onecx/portal-layout-styles": "^5.8.0", "@webcomponents/webcomponentsjs": "^2.8.0", "file-saver": "^2.0.5", "keycloak-angular": "^16.0.1", @@ -85,7 +85,7 @@ "ng-packagr": "18.1.0", "ngx-build-plus": "^18.0.0", "ngx-translate-testing": "^7.0.0", - "postcss": "8.4.40", + "postcss": "8.4.41", "postcss-import": "~16.1.0", "postcss-preset-env": "~9.6.0", "postcss-url": "~10.1.3", @@ -5776,18 +5776,20 @@ } }, "node_modules/@onecx/accelerator": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@onecx/accelerator/-/accelerator-5.4.0.tgz", - "integrity": "sha512-pEC9fSpSK4oaFWDIWwL5wVBc50JTLQvdNv5mUVvuPKmLPbLxC9P93VzylW/fP7AZII1nUO4w9Y3GdokzDqQH1Q==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@onecx/accelerator/-/accelerator-5.8.0.tgz", + "integrity": "sha512-MYV19RYf8rsOgujFtZef2ZTg/X+7hKsQEuvsN47v0x52sgoHS8c2eJvWUtgJ9I/CBA+ilQGolSDkv4rPSK5+jg==", + "license": "Apache-2.0", "peerDependencies": { "rxjs": "7.8.1", "tslib": "^2.6.3" } }, "node_modules/@onecx/angular-accelerator": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-accelerator/-/angular-accelerator-5.4.0.tgz", - "integrity": "sha512-3Fre2+SnMz+qTjfrIVtRAmtmJv0WZbqy19r+d5OFkmdTBfXySYF0DU2Oyxlc7XeroGeq8wz7J0jXblb1TmoXsQ==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-accelerator/-/angular-accelerator-5.8.0.tgz", + "integrity": "sha512-vBofOKz+p1x8+V/cVVlV9xM1KIxxoZACMh5JlNVjEDxZ+cs4un9EE/TjkArtzl32unJ+gHCPDntt1WlcnbRt2w==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" }, @@ -5809,9 +5811,10 @@ } }, "node_modules/@onecx/angular-auth": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-auth/-/angular-auth-5.4.0.tgz", - "integrity": "sha512-EyhOcTr0k7BaYTt6yvbKS9bpSqbdA75PsZpxHH1SHzoAVesJh0mhKsZX54oG33KEZD0sFsbZJvQq4hPpXZLCoQ==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-auth/-/angular-auth-5.8.0.tgz", + "integrity": "sha512-V90Ly1mHlGBht6VKJ1H2PEJ7mQMq3GSWpdep5cjyGY5SNCymd1rJexf96/bkWZf1lQ/IzIJbQBrFNlwdJiEXbA==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" }, @@ -5854,9 +5857,10 @@ } }, "node_modules/@onecx/angular-webcomponents": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-webcomponents/-/angular-webcomponents-5.4.0.tgz", - "integrity": "sha512-4tx6vU6WkWPGhllJZchYfv1GglPU66ecNCLaTTICXLcDLD5hsEJqa0zM33m/cvZwkcPbGDEbwy3gCg0TVH9koA==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-webcomponents/-/angular-webcomponents-5.8.0.tgz", + "integrity": "sha512-yZbTeytoxkDqo/40Mxr4g4T03MECvyhNrDxA3lX3lZMSkhUzp4UJYxTqj8DLZ/+HX3URdrih66VocqHMpHzEcw==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" }, @@ -5871,9 +5875,10 @@ } }, "node_modules/@onecx/integration-interface": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@onecx/integration-interface/-/integration-interface-5.4.0.tgz", - "integrity": "sha512-zujaWSjld/QrTRH6YqUp8n5IqI7Q9d/XB6pPHvHdcIvY/J+Eh0ngyb6wp26ayaIF+rAJ00PLDnRF+2B++vUdLg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@onecx/integration-interface/-/integration-interface-5.8.0.tgz", + "integrity": "sha512-PQ3m2ZBZDYk/gwbGnReyTFIKhlzgiEgt++K/xDu3+BBu4zxOAPHta/jUkg9fZAYEttHOMhGDl+/l1Or44tgSDA==", + "license": "Apache-2.0", "peerDependencies": { "@onecx/accelerator": "^5", "rxjs": "7.8.1", @@ -5881,9 +5886,10 @@ } }, "node_modules/@onecx/keycloak-auth": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@onecx/keycloak-auth/-/keycloak-auth-5.4.0.tgz", - "integrity": "sha512-vazfV1nSpKST+EhPN8alR9OQNTndhtZsu5AhAeRaZo/CbMadbUgFg2s9ArH6lgUDyCzcDMZdnfBgcfIKFeTVnw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@onecx/keycloak-auth/-/keycloak-auth-5.8.0.tgz", + "integrity": "sha512-+2rOYjdLWhvII7NH9cRgD5pRonB3TT4WqUPN+NpUv4GX5A2efFQKyjdDQLnzlI7e8uiqGGaa8dLXvX+nFgG/NQ==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" }, @@ -5898,9 +5904,10 @@ } }, "node_modules/@onecx/portal-integration-angular": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@onecx/portal-integration-angular/-/portal-integration-angular-5.4.0.tgz", - "integrity": "sha512-LuyoEZJcU1htv9V1nutfjMb3fb654lbvfnyxxL609MejNSsMekohjCT1olkju9DdVdNPmVsR5u19sH4knL6Lgg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@onecx/portal-integration-angular/-/portal-integration-angular-5.8.0.tgz", + "integrity": "sha512-aW5tTnj/ySGm8RBZsZnTAalFJQIBDVk/5g2kuz/liPKY1zUqhMxZQdkOlan8Mq/ZJRH1r/BvlJUYVbClvNkIdg==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" }, @@ -5932,9 +5939,10 @@ } }, "node_modules/@onecx/portal-layout-styles": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@onecx/portal-layout-styles/-/portal-layout-styles-5.4.0.tgz", - "integrity": "sha512-XDHset45XexQdtDNcI5hFRszVtsZobokhqlIx4/XK4ibFbk8qxDGzAoTFO8eazzhGQnd3rFpyPI0qjXcSd3FcA==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@onecx/portal-layout-styles/-/portal-layout-styles-5.8.0.tgz", + "integrity": "sha512-07V5zjOHNjvVYrkki5EasOsKoaHBZDNCA6ydHWW8tJ2nxlLyRb+/xDs9G39XANAgA+1Std9Ut6bXY6wq73DIgQ==", + "license": "Apache-2.0", "peerDependencies": { "tslib": "^2.6.3" } @@ -18239,9 +18247,9 @@ } }, "node_modules/postcss": { - "version": "8.4.40", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", - "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -18257,6 +18265,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", diff --git a/package.json b/package.json index b8c2c4c..3a48151 100644 --- a/package.json +++ b/package.json @@ -49,14 +49,14 @@ "@ngneat/error-tailor": "^5.0.1", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "@onecx/accelerator": "^5.4.0", - "@onecx/angular-auth": "^5.4.0", - "@onecx/angular-webcomponents": "^5.4.0", - "@onecx/angular-accelerator": "^5.4.0", - "@onecx/integration-interface": "^5.4.0", - "@onecx/keycloak-auth": "^5.4.0", - "@onecx/portal-integration-angular": "^5.4.0", - "@onecx/portal-layout-styles": "^5.4.0", + "@onecx/accelerator": "^5.8.0", + "@onecx/angular-auth": "^5.8.0", + "@onecx/angular-webcomponents": "^5.8.0", + "@onecx/angular-accelerator": "^5.8.0", + "@onecx/integration-interface": "^5.8.0", + "@onecx/keycloak-auth": "^5.8.0", + "@onecx/portal-integration-angular": "^5.8.0", + "@onecx/portal-layout-styles": "^5.8.0", "@webcomponents/webcomponentsjs": "^2.8.0", "file-saver": "^2.0.5", "keycloak-angular": "^16.0.1", @@ -111,7 +111,7 @@ "ng-packagr": "18.1.0", "ngx-build-plus": "^18.0.0", "ngx-translate-testing": "^7.0.0", - "postcss": "8.4.40", + "postcss": "8.4.41", "postcss-import": "~16.1.0", "postcss-preset-env": "~9.6.0", "postcss-url": "~10.1.3", diff --git a/src/app/parameter/parameter-detail/parameter-detail.component.spec.ts b/src/app/parameter/parameter-detail/parameter-detail.component.spec.ts new file mode 100644 index 0000000..45d2cb2 --- /dev/null +++ b/src/app/parameter/parameter-detail/parameter-detail.component.spec.ts @@ -0,0 +1,237 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { HttpClient } from '@angular/common/http' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { TranslateLoader, TranslateModule } from '@ngx-translate/core' +import { of, throwError } from 'rxjs' +import { FormControl, FormGroup } from '@angular/forms' + +import { + AppStateService, + createTranslateLoader, + PortalMessageService, + UserService +} from '@onecx/portal-integration-angular' +import { ApplicationParameter, ParametersAPIService, ProductStorePageResult } from 'src/app/shared/generated' +import { ParameterDetailComponent } from './parameter-detail.component' + +const productName = 'prod1' +const app = 'app1' + +const parameter: ApplicationParameter = { + id: 'id', + productName: productName, + applicationId: app, + key: 'key', + setValue: 'value' +} + +describe('ParameterDetailComponent', () => { + let component: ParameterDetailComponent + let fixture: ComponentFixture + + const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error']) + const apiServiceSpy = { + createParameterValue: jasmine.createSpy('createParameterValue').and.returnValue(of({})), + updateParameterValue: jasmine.createSpy('updateParameterValue').and.returnValue(of({})) + } + const formGroup = new FormGroup({ + id: new FormControl('id'), + key: new FormControl('key'), + value: new FormControl('value'), + productName: new FormControl('prod name'), + applicationId: new FormControl('app') + }) + const mockUserService = { + lang$: { + getValue: jasmine.createSpy('getValue') + } + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ParameterDetailComponent], + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient, AppStateService] + } + }) + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: PortalMessageService, useValue: msgServiceSpy }, + { provide: ParametersAPIService, useValue: apiServiceSpy }, + { provide: UserService, useValue: mockUserService } + ] + }).compileComponents() + msgServiceSpy.success.calls.reset() + msgServiceSpy.error.calls.reset() + apiServiceSpy.createParameterValue.calls.reset() + apiServiceSpy.updateParameterValue.calls.reset() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(ParameterDetailComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + afterEach(() => { + component.formGroup.reset() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('ngOnChange, i.e. opening detail dialog', () => { + it('should prepare editing an parameter', () => { + component.changeMode = 'EDIT' + component.parameter = parameter + + component.ngOnChanges() + + expect(component.parameterId).toEqual(parameter.id) + }) + + it('should prepare copying an parameter', () => { + component.changeMode = 'NEW' + component.parameter = parameter + component.ngOnChanges() + + expect(component.parameterId).toBeUndefined() + }) + + it('should prepare creating an parameter', () => { + component.changeMode = 'NEW' + spyOn(component.formGroup, 'reset') + + component.ngOnChanges() + + expect(component.formGroup.reset).toHaveBeenCalled() + }) + }) + + describe('onSave - creating and updating an parameter', () => { + it('should create an parameter', () => { + apiServiceSpy.createParameterValue.and.returnValue(of({})) + component.changeMode = 'NEW' + spyOn(component.hideDialogAndChanged, 'emit') + component.formGroup = formGroup + + component.onSave() + + expect(msgServiceSpy.success).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.CREATE.MESSAGE.OK' + }) + expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(true) + }) + + it('should display error if creation fails', () => { + apiServiceSpy.createParameterValue.and.returnValue(throwError(() => new Error())) + component.changeMode = 'NEW' + component.formGroup = formGroup + + component.onSave() + + expect(component.formGroup.valid).toBeTrue() + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.CREATE.MESSAGE.NOK' + }) + }) + + it('should update an parameter', () => { + apiServiceSpy.updateParameterValue.and.returnValue(of({})) + component.changeMode = 'EDIT' + spyOn(component.hideDialogAndChanged, 'emit') + component.parameterId = 'id' + component.formGroup = formGroup + + component.onSave() + + expect(msgServiceSpy.success).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.MESSAGE.OK' + }) + expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(true) + }) + + it('should display error if update fails', () => { + apiServiceSpy.updateParameterValue.and.returnValue(throwError(() => new Error())) + component.changeMode = 'EDIT' + component.parameterId = 'id' + component.formGroup = formGroup + + component.onSave() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.MESSAGE.NOK' + }) + }) + }) + + describe('updateApplicationIds', () => { + it('should update applicationIds based on the product name', () => { + component.products = { + stream: [ + { + productName: 'Product A', + applications: ['App1', 'App2'] + }, + { + productName: 'Product B', + applications: ['App3'] + } + ] + } as ProductStorePageResult + + component.updateApplicationIds('Product A') + + expect(component.applicationIds).toEqual([ + { label: 'App1', value: 'App1' }, + { label: 'App2', value: 'App2' } + ]) + expect(component.formGroup.controls['applicationId'].value).toBeNull() + }) + + it('should clear applicationIds if productName does not match', () => { + component.products = { + stream: [ + { + productName: 'Product A', + applications: ['App1', 'App2'] + } + ] + } as ProductStorePageResult + + component.updateApplicationIds('Product C') + + expect(component.applicationIds).toEqual([]) + expect(component.formGroup.controls['applicationId'].value).toBeNull() + }) + + it('should handle empty or undefined products', () => { + component.products = undefined + + component.updateApplicationIds('Product A') + + expect(component.applicationIds).toEqual([]) + + expect(component.formGroup.controls['applicationId'].value).toBeNull() + }) + }) + + /* + * UI ACTIONS + */ + it('should close the dialog', () => { + spyOn(component.hideDialogAndChanged, 'emit') + component.onDialogHide() + + expect(component.displayDetailDialog).toBeFalse() + expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(false) + }) +}) diff --git a/src/app/parameter/parameter-detail/parameter-detail.component.ts b/src/app/parameter/parameter-detail/parameter-detail.component.ts index f57bcf0..ee50d9b 100644 --- a/src/app/parameter/parameter-detail/parameter-detail.component.ts +++ b/src/app/parameter/parameter-detail/parameter-detail.component.ts @@ -88,7 +88,6 @@ export class ParameterDetailComponent implements OnChanges { value: app }) }) - this.formGroup.controls['applicationId'].enable() } }) } @@ -133,7 +132,6 @@ export class ParameterDetailComponent implements OnChanges { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private submitFormValues(): any { const parameter: ApplicationParameter = { ...this.formGroup.value } return parameter diff --git a/src/app/parameter/parameter-detail/parameter-detailcomponent.spec.ts b/src/app/parameter/parameter-detail/parameter-detailcomponent.spec.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/parameter/parameter-detail/parameter-history/parameter-history.component.spec.ts b/src/app/parameter/parameter-detail/parameter-history/parameter-history.component.spec.ts index e69de29..b9fdd99 100644 --- a/src/app/parameter/parameter-detail/parameter-history/parameter-history.component.spec.ts +++ b/src/app/parameter/parameter-detail/parameter-history/parameter-history.component.spec.ts @@ -0,0 +1,119 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { HttpClient } from '@angular/common/http' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { TranslateLoader, TranslateModule } from '@ngx-translate/core' +import { of } from 'rxjs' +import { DatePipe } from '@angular/common' +import { FormBuilder } from '@angular/forms' + +import { AppStateService, createTranslateLoader, PortalMessageService } from '@onecx/portal-integration-angular' + +import { ParametersAPIService, HistoriesAPIService, ApplicationParameter } from 'src/app/shared/generated' +import { ParameterHistoryComponent } from './parameter-history.component' + +const productName = 'prod1' +const app = 'app1' + +const parameter: ApplicationParameter = { + id: 'id', + productName: productName, + applicationId: app, + key: 'key', + setValue: 'value' +} + +describe('ParameterHistoryComponent', () => { + let component: ParameterHistoryComponent + let fixture: ComponentFixture + let datePipe: DatePipe + + const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error']) + const apiServiceSpy = { + getParameterById: jasmine.createSpy('getParameterById').and.returnValue(of({})) + } + const historyServiceSpy = { + getAllApplicationParametersHistory: jasmine.createSpy('getAllApplicationParametersHistory').and.returnValue(of({})), + getCountsByCriteria: jasmine.createSpy('getCountsByCriteria').and.returnValue(of({})) + } + + beforeEach(waitForAsync(() => { + datePipe = new DatePipe('en-US') + + TestBed.configureTestingModule({ + declarations: [ParameterHistoryComponent], + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient, AppStateService] + } + }) + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + FormBuilder, + { provide: PortalMessageService, useValue: msgServiceSpy }, + { provide: ParametersAPIService, useValue: apiServiceSpy }, + { provide: HistoriesAPIService, useValue: historyServiceSpy }, + { provide: DatePipe, useValue: datePipe } + ] + }).compileComponents() + msgServiceSpy.success.calls.reset() + msgServiceSpy.error.calls.reset() + apiServiceSpy.getParameterById.calls.reset() + historyServiceSpy.getAllApplicationParametersHistory.calls.reset() + historyServiceSpy.getCountsByCriteria.calls.reset() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(ParameterHistoryComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + afterEach(() => { + component.formGroup.reset() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('ngOnChanges', () => { + it('should call getParameter and loadTranslations if parameter is defined', () => { + component.parameter = parameter + + spyOn(component as any, 'getParameter') + spyOn(component as any, 'loadTranslations') + + component.ngOnChanges() + + expect(component['getParameter']).toHaveBeenCalledWith('id') + expect(component['loadTranslations']).toHaveBeenCalled() + }) + + it('should only loadTranslations if parameter is undefined', () => { + spyOn(component as any, 'getParameter') + spyOn(component as any, 'loadTranslations') + + component.ngOnChanges() + + expect(component['getParameter']).not.toHaveBeenCalled() + expect(component['loadTranslations']).toHaveBeenCalled() + }) + }) + + /* + * UI ACTIONS + */ + it('should close the dialog', () => { + spyOn(component.hideDialog, 'emit') + component.onDialogHide() + + expect(component.displayHistoryDialog).toBeFalse() + expect(component.hideDialog.emit).toHaveBeenCalled() + }) +}) diff --git a/src/app/parameter/parameter-search/parameter-search.component.spec.ts b/src/app/parameter/parameter-search/parameter-search.component.spec.ts index 45f3250..d2e68ed 100644 --- a/src/app/parameter/parameter-search/parameter-search.component.spec.ts +++ b/src/app/parameter/parameter-search/parameter-search.component.spec.ts @@ -1,23 +1,71 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' import { HttpClientTestingModule } from '@angular/common/http/testing' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' -import { RouterTestingModule } from '@angular/router/testing' -import { TranslateModule, TranslateService } from '@ngx-translate/core' -import { MessageService } from 'primeng/api' +import { HttpClient } from '@angular/common/http' +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' import { TranslateServiceMock } from '../../shared/TranslateServiceMock' +import { of, throwError } from 'rxjs' + +import { AppStateService, Column, createTranslateLoader, PortalMessageService } from '@onecx/portal-integration-angular' import { ParameterSearchComponent } from './parameter-search.component' -import { SharedModule } from '../../shared/shared.module' +import { + ApplicationParameter, + ParametersAPIService, + ProductsAPIService, + ProductStorePageResult +} from 'src/app/shared/generated' +import { SelectItem } from 'primeng/api' + +const parameterData: ApplicationParameter[] = [ + { id: 'id', productName: 'prod1', applicationId: 'app1', key: 'key1', setValue: 'value1' }, + { id: 'id2', productName: 'prod2', applicationId: 'app2', key: 'key2', setValue: 'value2' }, + { id: 'id3', productName: 'prod3', applicationId: 'app3', key: 'key3', setValue: 'value3' } +] describe('ParameterSearchComponent', () => { let component: ParameterSearchComponent let fixture: ComponentFixture + const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) + const apiServiceSpy = { + searchApplicationParametersByCriteria: jasmine + .createSpy('searchApplicationParametersByCriteria') + .and.returnValue(of({})), + deleteParameter: jasmine.createSpy('deleteParameter').and.returnValue(of({})) + } + const productApiSpy = { + searchAllAvailableProducts: jasmine.createSpy('searchAllAvailableProducts').and.returnValue(of({})), + deleteParameter: jasmine.createSpy('deleteParameter').and.returnValue(of({})) + } + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ParameterSearchComponent], - imports: [HttpClientTestingModule, TranslateModule, RouterTestingModule, SharedModule], - providers: [{ provide: TranslateService, useClass: TranslateServiceMock }, MessageService] + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot({ + isolate: true, + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient, AppStateService] + } + }) + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: TranslateService, useClass: TranslateServiceMock }, + { provide: PortalMessageService, useValue: msgServiceSpy }, + { provide: ParametersAPIService, useValue: apiServiceSpy }, + { provide: ProductsAPIService, useValue: productApiSpy } + ] }).compileComponents() + msgServiceSpy.success.calls.reset() + msgServiceSpy.error.calls.reset() + apiServiceSpy.searchApplicationParametersByCriteria.calls.reset() + apiServiceSpy.deleteParameter.calls.reset() + productApiSpy.searchAllAvailableProducts.calls.reset() })) beforeEach(() => { @@ -29,4 +77,245 @@ describe('ParameterSearchComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) + + describe('search', () => { + it('should search parameters without search criteria', () => { + apiServiceSpy.searchApplicationParametersByCriteria.and.returnValue(of({ stream: parameterData })) + + component.search({}) + + component.results$?.subscribe({ + next: (data) => { + expect(data).toEqual(parameterData) + } + }) + }) + + it('should display an info message if there are no parameters', () => { + apiServiceSpy.searchApplicationParametersByCriteria.and.returnValue(of({ totalElements: 0, stream: [] })) + + component.search({}) + + component.results$?.subscribe({ + next: (data) => { + expect(data.length).toEqual(0) + expect(msgServiceSpy.info).toHaveBeenCalledOnceWith({ summaryKey: 'SEARCH.MSG_NO_RESULTS' }) + } + }) + }) + + it('should display an error message if the search call fails', () => { + const err = { status: '400' } + apiServiceSpy.searchApplicationParametersByCriteria.and.returnValue(throwError(() => err)) + + component.search({}) + + component.results$?.subscribe({ + error: () => { + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'SEARCH.MSG_SEARCH_FAILED' + }) + } + }) + }) + }) + + describe('getAllProductNames', () => { + it('should log an error if the API call fails', () => { + const mockError = new Error('API error') + spyOn(console, 'error') + productApiSpy.searchAllAvailableProducts.and.returnValue(throwError(() => mockError)) + + component['getAllProductNames']() + + component.products$!.subscribe((data) => { + expect(data).toEqual([]) + }) + + expect(console.error).toHaveBeenCalledWith('getAllProductNames():', mockError) + }) + + it('should set allProductNames$ observable and map product names correctly', () => { + const mockProductStorePageResult: ProductStorePageResult = { + stream: [{ productName: 'Product A' }, { productName: 'Product B' }] + } as ProductStorePageResult + const sortedItems: SelectItem[] = [ + { label: 'Product A', value: 'Product A' }, + { label: 'Product B', value: 'Product B' } + ] + productApiSpy.searchAllAvailableProducts.and.returnValue(of(mockProductStorePageResult)) + + component['getAllProductNames']() + + component.allProductNames$!.subscribe((data) => { + expect(data).toEqual(sortedItems) + }) + }) + }) + + /* + * UI ACTIONS + */ + it('should prepare the creation of a new parameter', () => { + component.onCreate() + + expect(component.changeMode).toEqual('NEW') + expect(component.usedProductsChanged).toBeFalse() + expect(component.parameter).toBe(undefined) + expect(component.displayDetailDialog).toBeTrue() + }) + + it('should show details of a parameter', () => { + const ev: MouseEvent = new MouseEvent('type') + spyOn(ev, 'stopPropagation') + const mode = 'EDIT' + + component.onDetail(ev, parameterData[0], mode) + + expect(ev.stopPropagation).toHaveBeenCalled() + expect(component.changeMode).toEqual(mode) + expect(component.usedProductsChanged).toBeFalse() + expect(component.parameter).toBe(parameterData[0]) + expect(component.displayDetailDialog).toBeTrue() + }) + + it('should prepare the copy of a parameter', () => { + const ev: MouseEvent = new MouseEvent('type') + spyOn(ev, 'stopPropagation') + + component.onCopy(ev, parameterData[0]) + + expect(ev.stopPropagation).toHaveBeenCalled() + expect(component.changeMode).toEqual('NEW') + expect(component.usedProductsChanged).toBeFalse() + expect(component.parameter).toBe(parameterData[0]) + expect(component.displayDetailDialog).toBeTrue() + }) + + it('should prepare the deletion of a parameter', () => { + const ev: MouseEvent = new MouseEvent('type') + spyOn(ev, 'stopPropagation') + + component.onDelete(ev, parameterData[0]) + + expect(ev.stopPropagation).toHaveBeenCalled() + expect(component.usedProductsChanged).toBeFalse() + expect(component.parameter).toBe(parameterData[0]) + expect(component.displayDeleteDialog).toBeTrue() + }) + + it('should delete a parameter item', () => { + const ev: MouseEvent = new MouseEvent('type') + apiServiceSpy.deleteParameter.and.returnValue(of({})) + component.parameters = [ + { id: 'a1', key: 'a1' }, + { id: 'a2', key: 'a2', productName: 'prod' } + ] + component.onDelete(ev, component.parameters[0]) + component.onDeleteConfirmation() + + expect(component.parameters.length).toBe(1) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGES.OK' }) + + component.onDelete(ev, component.parameters[0]) + component.onDeleteConfirmation() + expect(component.parameters.length).toBe(0) + }) + + it('should display error if deleting an parameter fails', () => { + apiServiceSpy.deleteParameter.and.returnValue(throwError(() => new Error())) + component.parameter = { + id: 'definedHere' + } + component.parameters = [{ id: 'id', productName: 'prod1', applicationId: 'app1', key: 'key1', setValue: 'value1' }] + + component.onDeleteConfirmation() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.DELETE.MESSAGES.NOK' + }) + }) + + it('should set correct values when detail dialog is closed', () => { + spyOn(component, 'search') + + component.onCloseDetail(true) + + expect(component.search).toHaveBeenCalled() + expect(component.displayDeleteDialog).toBeFalse() + }) + + it('should update the columns that are seen in results', () => { + const columns: Column[] = [ + { + field: 'productName', + header: 'PRODUCT_NAME' + }, + { + field: 'description', + header: 'DESCRIPTION' + } + ] + const expectedColumn = { field: 'productName', header: 'PRODUCT_NAME' } + component.columns = columns + + component.onColumnsChange(['productName']) + + expect(component.filteredColumns).not.toContain(columns[1]) + expect(component.filteredColumns).toEqual([jasmine.objectContaining(expectedColumn)]) + }) + + it('should apply a filter to the result table', () => { + component.parameterTable = jasmine.createSpyObj('parameterTable', ['filterGlobal']) + + component.onFilterChange('test') + + expect(component.parameterTable?.filterGlobal).toHaveBeenCalledWith('test', 'contains') + }) + + it('should open create dialog', () => { + spyOn(component, 'onCreate') + + component.ngOnInit() + component.actions$?.subscribe((action) => { + action[0].actionCallback() + }) + + expect(component.onCreate).toHaveBeenCalled() + }) + + describe('onHistory', () => { + it('should stop event propagation, set parameter, and display history dialog', () => { + const event = new MouseEvent('click') + spyOn(event, 'stopPropagation') + + component.onHistory(event, parameterData[0]) + + expect(event.stopPropagation).toHaveBeenCalled() + expect(component.parameter).toEqual(parameterData[0]) + expect(component.displayHistoryDialog).toBeTrue() + }) + }) + + it('should hide the history dialog', () => { + component.displayHistoryDialog = true + + component.onCloseHistory() + + expect(component.displayHistoryDialog).toBeFalse() + }) + + describe('onReset', () => { + it('should reset criteria, reset the form group, and disable the applicationId control', () => { + component.criteria = { key: 'key' } + component.criteriaGroup.controls['applicationId'].enable() + + component.onReset() + + expect(component.criteria).toEqual({}) + expect(component.criteriaGroup.pristine).toBeTrue() + expect(component.criteriaGroup.dirty).toBeFalse() + expect(component.criteriaGroup.controls['applicationId'].disabled).toBeTrue() + }) + }) }) diff --git a/src/app/parameter/parameter-search/parameter-search.component.ts b/src/app/parameter/parameter-search/parameter-search.component.ts index 196c60c..3ac90a3 100644 --- a/src/app/parameter/parameter-search/parameter-search.component.ts +++ b/src/app/parameter/parameter-search/parameter-search.component.ts @@ -161,7 +161,7 @@ export class ParameterSearchComponent implements OnInit { tap({ next: (data: any) => { if (data.totalElements == 0) { - this.messageService.success({ + this.messageService.info({ summaryKey: 'SEARCH.MSG_NO_RESULTS' }) return data.size