From 4b7750f492b1eccdd2191bda441f8832fa505f10 Mon Sep 17 00:00:00 2001 From: Christian Badura <93912698+cbadura@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:44:00 +0100 Subject: [PATCH] Adjust tests (#43) * feat: finish delete, advance detail * feat: finish app detail * feat: make progress with app search * feat: finish app search * feat: finish utils * feat: finish product search tests * feat: finish product detail tests * feat: almost finish product apps tests * feat: finish product apps * feat: finish tests --------- Co-authored-by: Christian Badura --- .../app-delete/app-delete.component.spec.ts | 51 +++ .../app-detail/app-detail.component.spec.ts | 312 ++++++++++++++++++ .../app-detail/app-detail.component.ts | 2 +- .../app-search/app-search.component.spec.ts | 226 +++++++++++++ .../product-apps.component.spec.ts | 121 ++++++- .../product-apps/product-apps.component.ts | 9 +- .../product-intern.component.spec.ts | 37 +++ .../product.detail.component.spec.ts | 180 +++++++--- ...s_nok => product-search.component.spec.ts} | 44 +-- src/app/product-store/product-store.module.ts | 2 +- src/app/shared/utils.spec.ts | 69 +++- 11 files changed, 967 insertions(+), 86 deletions(-) create mode 100644 src/app/product-store/app-delete/app-delete.component.spec.ts create mode 100644 src/app/product-store/app-detail/app-detail.component.spec.ts create mode 100644 src/app/product-store/app-search/app-search.component.spec.ts create mode 100644 src/app/product-store/product-detail/product-intern/product-intern.component.spec.ts rename src/app/product-store/product-search/{product-search.component.spec.ts_nok => product-search.component.spec.ts} (74%) diff --git a/src/app/product-store/app-delete/app-delete.component.spec.ts b/src/app/product-store/app-delete/app-delete.component.spec.ts new file mode 100644 index 0000000..b859711 --- /dev/null +++ b/src/app/product-store/app-delete/app-delete.component.spec.ts @@ -0,0 +1,51 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { TranslateTestingModule } from 'ngx-translate-testing' + +import { AppDeleteComponent } from './app-delete.component' + +describe('AppDeleteComponent', () => { + let component: AppDeleteComponent + let fixture: ComponentFixture + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [AppDeleteComponent], + imports: [ + HttpClientTestingModule, + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(AppDeleteComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should behave correctly onDialogHide', () => { + spyOn(component.displayDialogChange, 'emit') + + component.onDialogHide() + + expect(component.displayDialogChange.emit).toHaveBeenCalledWith(false) + }) + + it('should confirmDeletion', () => { + spyOn(console, 'log') + + component.confirmDeletion() + + expect(console.log).toHaveBeenCalledWith('confirmDeletion') + }) +}) diff --git a/src/app/product-store/app-detail/app-detail.component.spec.ts b/src/app/product-store/app-detail/app-detail.component.spec.ts new file mode 100644 index 0000000..299f1b8 --- /dev/null +++ b/src/app/product-store/app-detail/app-detail.component.spec.ts @@ -0,0 +1,312 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { RouterTestingModule } from '@angular/router/testing' +import { of, throwError } from 'rxjs' +import { FormControl, FormGroup, Validators } from '@angular/forms' +import { TranslateTestingModule } from 'ngx-translate-testing' + +import { PortalMessageService, ConfigurationService, UserService } from '@onecx/portal-integration-angular' +import { AppDetailComponent, AppDetailForm } from './app-detail.component' +import { MicrofrontendsAPIService, Microfrontend } from 'src/app/shared/generated' + +const form = new FormGroup({ + appId: new FormControl('id', Validators.minLength(2)), + appName: new FormControl(''), + appVersion: new FormControl(''), + productName: new FormControl(''), + description: new FormControl(''), + technology: new FormControl(''), + remoteBaseUrl: new FormControl(''), + remoteEntry: new FormControl(''), + exposedModule: new FormControl(''), + classifications: new FormControl(''), + contact: new FormControl(''), + iconName: new FormControl(''), + note: new FormControl('') +}) + +const app: Microfrontend = { + appId: 'appId', + id: 'id', + appName: 'name', + remoteBaseUrl: 'url', + productName: 'productName', + appVersion: 'version', + remoteEntry: 'entry', + description: 'description', + technology: 'technology', + contact: 'contact', + iconName: 'iconName', + note: 'note', + exposedModule: 'exposedModule', + classifications: ['classifications'] +} + +describe('AppDetailComponent', () => { + let component: AppDetailComponent + let fixture: ComponentFixture + + const apiServiceSpy = { + getMicrofrontendByAppId: jasmine.createSpy('getMicrofrontendByAppId').and.returnValue(of({})), + createMicrofrontend: jasmine.createSpy('createMicrofrontend').and.returnValue(of({})), + updateMicrofrontend: jasmine.createSpy('updateMicrofrontend').and.returnValue(of({})) + } + const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error']) + const configServiceSpy = { + lang: 'en', + getProperty: jasmine.createSpy('getProperty').and.returnValue('123'), + getPortal: jasmine.createSpy('getPortal').and.returnValue({ + themeId: '1234', + portalName: 'test', + baseUrl: '/', + microfrontendRegistrations: [] + }) + } + const mockUserService = { + lang$: { + getValue: jasmine.createSpy('getValue').and.returnValue('en') + }, + hasPermission: jasmine.createSpy('hasPermission').and.callFake((permissionName) => { + if (permissionName === 'MICROFRONTEND#CREATE') { + return true + } else if (permissionName === 'MICROFRONTEND#EDIT') { + return true + } else { + return false + } + }) + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [AppDetailComponent], + imports: [ + RouterTestingModule, + HttpClientTestingModule, + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') + ], + providers: [ + { provide: MicrofrontendsAPIService, useValue: apiServiceSpy }, + { provide: PortalMessageService, useValue: msgServiceSpy }, + { provide: ConfigurationService, useValue: configServiceSpy }, + { provide: UserService, useValue: mockUserService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(AppDetailComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + afterEach(() => { + msgServiceSpy.success.calls.reset() + msgServiceSpy.error.calls.reset() + apiServiceSpy.getMicrofrontendByAppId.calls.reset() + apiServiceSpy.createMicrofrontend.calls.reset() + apiServiceSpy.updateMicrofrontend.calls.reset() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should get call getApp onChanges if not create mode', () => { + component.appAbstract = { + appId: 'appId', + appName: 'name', + remoteBaseUrl: 'url', + id: 'id', + productName: 'productName' + } + component.displayDetailDialog = true + component.changeMode = 'EDIT' + spyOn(component, 'getApp') + + component.ngOnChanges() + + expect(component.getApp).toHaveBeenCalled() + }) + + it('should set app to undefined onChanges in create mode', () => { + component.appAbstract = { + appId: 'appId', + appName: 'name', + remoteBaseUrl: 'url', + id: 'id', + productName: 'productName' + } + component.displayDetailDialog = true + component.changeMode = 'CREATE' + spyOn(component, 'getApp') + + component.ngOnChanges() + + expect(component.app).toBeUndefined() + }) + + it('should getApp', () => { + apiServiceSpy.getMicrofrontendByAppId.and.returnValue(of(app)) + component.formGroup = form + + component.getApp() + + expect(component.app).toBe(app) + }) + + it('should getApp and prepare copy', () => { + apiServiceSpy.getMicrofrontendByAppId.and.returnValue(of(app)) + component.formGroup = form + component.changeMode = 'COPY' + component.app = app + + component.getApp() + + expect(component.app.id).toBeUndefined() + }) + + it('should behave correctly onDialogHide', () => { + spyOn(component.displayDetailDialogChange, 'emit') + + component.onDialogHide() + + expect(component.displayDetailDialogChange.emit).toHaveBeenCalledWith(false) + }) + + it('should display error if form is invalid onSave', () => { + component.formGroup = new FormGroup({ + appId: new FormControl('i', Validators.minLength(2)), + appName: new FormControl(''), + appVersion: new FormControl(''), + productName: new FormControl(''), + description: new FormControl(''), + technology: new FormControl(''), + remoteBaseUrl: new FormControl(''), + remoteEntry: new FormControl(''), + exposedModule: new FormControl(''), + classifications: new FormControl(''), + contact: new FormControl(''), + iconName: new FormControl(''), + note: new FormControl('') + }) + component.changeMode = 'CREATE' + + component.onSave() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'VALIDATION.FORM_INVALID' }) + }) + + it('should call createApp onSave in create mode', () => { + apiServiceSpy.createMicrofrontend.and.returnValue(of({})) + component.formGroup = form + component.changeMode = 'CREATE' + + component.onSave() + + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.APP.OK' }) + }) + + it('should display save error in create mode', () => { + const err = { + error: { + detail: 'Error', + errorCode: 'PERSIST_ENTITY_FAILED' + } + } + apiServiceSpy.createMicrofrontend.and.returnValue(throwError(() => err)) + component.formGroup = form + component.changeMode = 'CREATE' + + component.onSave() + + const expectedKey = '' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.CREATE.APP.NOK', + detailKey: expectedKey + }) + }) + + it('should call updateApp onSave in edit mode', () => { + apiServiceSpy.updateMicrofrontend.and.returnValue(of({})) + component.formGroup = form + component.changeMode = 'EDIT' + + component.onSave() + + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.APP.OK' }) + }) + + it('should display save error in edit mode: unique constraint app id', () => { + const err = { + error: { + detail: 'error: microfrontend_app_id', + errorCode: 'PERSIST_ENTITY_FAILED' + } + } + apiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.formGroup = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.APP_ID' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) + }) + + it('should display save error in edit mode: unique constraint app id', () => { + const err = { + error: { + detail: 'error: microfrontend_remote_module', + errorCode: 'PERSIST_ENTITY_FAILED' + } + } + apiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.formGroup = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.APP.UNIQUE_CONSTRAINT.REMOTE_MODULE' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) + }) + + it('should display save error in edit mode: other internal error', () => { + const err = { + error: { + detail: 'error: microfrontend_remote_module', + errorCode: 'other' + } + } + apiServiceSpy.updateMicrofrontend.and.returnValue(throwError(() => err)) + component.formGroup = form + component.changeMode = 'EDIT' + + component.onSave() + + const expectedKey = 'VALIDATION.ERRORS.INTERNAL_ERROR' + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.APP.NOK', + detailKey: expectedKey + }) + }) + + it('should call this.user.lang$ from the constructor and set this.dateFormat to the default format if user.lang$ is not de', () => { + mockUserService.lang$.getValue.and.returnValue('de') + fixture = TestBed.createComponent(AppDetailComponent) + component = fixture.componentInstance + fixture.detectChanges() + expect(component.dateFormat).toEqual('dd.MM.yyyy HH:mm:ss') + }) +}) diff --git a/src/app/product-store/app-detail/app-detail.component.ts b/src/app/product-store/app-detail/app-detail.component.ts index e79ab71..625f286 100644 --- a/src/app/product-store/app-detail/app-detail.component.ts +++ b/src/app/product-store/app-detail/app-detail.component.ts @@ -16,7 +16,7 @@ import { export type ChangeMode = 'VIEW' | 'CREATE' | 'EDIT' | 'COPY' -interface AppDetailForm { +export interface AppDetailForm { appId: FormControl appName: FormControl appVersion: FormControl diff --git a/src/app/product-store/app-search/app-search.component.spec.ts b/src/app/product-store/app-search/app-search.component.spec.ts new file mode 100644 index 0000000..cf174db --- /dev/null +++ b/src/app/product-store/app-search/app-search.component.spec.ts @@ -0,0 +1,226 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { RouterTestingModule } from '@angular/router/testing' +import { Router, ActivatedRoute } from '@angular/router' +import { of } from 'rxjs' +import { FormControl, FormGroup, Validators } from '@angular/forms' +import { TranslateTestingModule } from 'ngx-translate-testing' + +import { UserService } from '@onecx/portal-integration-angular' +import { AppSearchComponent, MicrofrontendSearchCriteria } from './app-search.component' +import { MicrofrontendsAPIService /* , Microfrontend */ } from 'src/app/shared/generated' + +const form = new FormGroup({ + appId: new FormControl(null, Validators.minLength(2)), + appName: new FormControl(null), + productName: new FormControl(null) +}) + +describe('AppSearchComponent', () => { + let component: AppSearchComponent + let fixture: ComponentFixture + let routerSpy = jasmine.createSpyObj('Router', ['navigate']) + let routeMock = { snapshot: { paramMap: new Map() } } + + const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['get']) + const apiServiceSpy = { + searchMicrofrontends: jasmine.createSpy('searchMicrofrontends').and.returnValue(of({})) + } + const mockUserService = { + lang$: { + getValue: jasmine.createSpy('getValue').and.returnValue('en') + }, + hasPermission: jasmine.createSpy('hasPermission').and.callFake((permissionName) => { + if (permissionName === 'MICROFRONTEND#CREATE') { + return true + } else if (permissionName === 'MICROFRONTEND#EDIT') { + return true + } else { + return false + } + }) + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [AppSearchComponent], + imports: [ + RouterTestingModule, + HttpClientTestingModule, + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') + ], + providers: [ + { provide: MicrofrontendsAPIService, useValue: apiServiceSpy }, + { provide: UserService, useValue: mockUserService }, + { provide: Router, useValue: routerSpy }, + { provide: ActivatedRoute, useValue: routeMock } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(AppSearchComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + afterEach(() => { + apiServiceSpy.searchMicrofrontends.calls.reset() + translateServiceSpy.get.calls.reset() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should call onCreate when actionCallback is executed', () => { + spyOn(component, 'onCreate') + + component.ngOnInit() + + if (component.actions$) { + component.actions$.subscribe((actions) => { + const firstAction = actions[1] + firstAction.actionCallback() + expect(component.onCreate).toHaveBeenCalled() + }) + } + }) + + it('should call onBack when actionCallback is executed', () => { + spyOn(component, 'onBack') + + component.ngOnInit() + + if (component.actions$) { + component.actions$.subscribe((actions) => { + const firstAction = actions[0] + firstAction.actionCallback() + expect(component.onBack).toHaveBeenCalled() + }) + } + }) + + it('should update viewMode onLayoutChange', () => { + component.onLayoutChange('EDIT') + + expect(component.viewMode).toBe('EDIT') + }) + + it('should update filter and call dv.filter onFilterChange', () => { + const filter = 'testFilter' + + component.onFilterChange(filter) + + expect(component.filter).toBe(filter) + }) + + it('should update sortField onSortChange', () => { + component.onSortChange('field') + + expect(component.sortField).toBe('field') + }) + + it('should update sortOrder based on asc boolean onSortDirChange', () => { + component.onSortDirChange(true) + expect(component.sortOrder).toBe(-1) + + component.onSortDirChange(false) + expect(component.sortOrder).toBe(1) + }) + + it('should call searchApps onSearch', () => { + spyOn(component, 'searchApps') + + component.onSearch() + + expect(component.searchApps).toHaveBeenCalled() + }) + + it('should reset appSearchCriteriaGroup onSearchReset is called', () => { + component.appSearchCriteriaGroup = form + spyOn(form, 'reset').and.callThrough() + + component.onSearchReset() + + expect(component.appSearchCriteriaGroup.reset).toHaveBeenCalled() + }) + + it('should navigate back onBack', () => { + component.onBack() + + expect(routerSpy.navigate).toHaveBeenCalledWith(['../'], { relativeTo: routeMock }) + }) + + it('should stop event propagation and navigate to the product onGotoProduct', () => { + const event = { stopPropagation: jasmine.createSpy() } + + component.onGotoProduct(event as any, 'product') + + expect(event.stopPropagation).toHaveBeenCalled() + expect(routerSpy.navigate).toHaveBeenCalledWith(['../', 'product'], { relativeTo: routeMock }) + }) + + it('should should assign app to component property and change to edit mode onDetail', () => { + const event = { stopPropagation: jasmine.createSpy() } + const app = { + id: 'id', + appId: 'appId', + appName: 'appName', + remoteBaseUrl: 'url', + productName: 'product' + } + + component.onDetail(event as any, app) + + expect(event.stopPropagation).toHaveBeenCalled() + expect(component.app).toBe(app) + expect(component.changeMode).toBe('EDIT') + }) + + it('should should assign app to component property and change to copy mode onCopy', () => { + const event = { stopPropagation: jasmine.createSpy() } + const app = { + id: 'id', + appId: 'appId', + appName: 'appName', + remoteBaseUrl: 'url', + productName: 'product' + } + + component.onCopy(event as any, app) + + expect(event.stopPropagation).toHaveBeenCalled() + expect(component.app).toBe(app) + expect(component.changeMode).toBe('COPY') + }) + + it('should should assign app to component property and change to copy mode onCreate', () => { + component.onCreate() + + expect(component.app).toBeUndefined() + expect(component.changeMode).toBe('CREATE') + expect(component.displayDetailDialog).toBeTrue() + }) + + it('should should assign app to component property and change to copy mode onDelete', () => { + const event = { stopPropagation: jasmine.createSpy() } + const app = { + id: 'id', + appId: 'appId', + appName: 'appName', + remoteBaseUrl: 'url', + productName: 'product' + } + + component.onDelete(event as any, app) + + expect(event.stopPropagation).toHaveBeenCalled() + expect(component.app).toBe(app) + }) +}) diff --git a/src/app/product-store/product-detail/product-apps/product-apps.component.spec.ts b/src/app/product-store/product-detail/product-apps/product-apps.component.spec.ts index 2f9af07..ab6e38e 100644 --- a/src/app/product-store/product-detail/product-apps/product-apps.component.spec.ts +++ b/src/app/product-store/product-detail/product-apps/product-apps.component.spec.ts @@ -7,15 +7,29 @@ import { TranslateTestingModule } from 'ngx-translate-testing' import { PortalMessageService } from '@onecx/portal-integration-angular' import { ProductAppsComponent } from './product-apps.component' -import { ProductsAPIService } from 'src/app/shared/generated' +import { ProductsAPIService, Product, MicrofrontendAbstract } from 'src/app/shared/generated' -describe('ProductAppComponent', () => { +const product: Product = { + id: 'id', + name: 'name', + basePath: 'path' +} + +const mockApp: MicrofrontendAbstract = { + appId: 'appId', + appName: 'appName', + id: 'id', + productName: 'prodName', + remoteBaseUrl: 'url' +} + +describe('ProductAppsComponent', () => { let component: ProductAppsComponent let fixture: ComponentFixture const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) const apiServiceSpy = { - createProduct: jasmine.createSpy('createProduct').and.returnValue(of({})), + searchMicrofrontends: jasmine.createSpy('searchMicrofrontends').and.returnValue(of({})), updateProduct: jasmine.createSpy('updateProduct').and.returnValue(of({})) } @@ -45,14 +59,111 @@ describe('ProductAppComponent', () => { }) afterEach(() => { + apiServiceSpy.searchMicrofrontends.calls.reset() msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() msgServiceSpy.info.calls.reset() - apiServiceSpy.createProduct.calls.reset() - apiServiceSpy.updateProduct.calls.reset() }) it('should create', () => { expect(component).toBeTruthy() }) + + it('should call loadApps onChanges if product exists', () => { + component.product = product + spyOn(component, 'loadApps') + + component.ngOnChanges() + + expect(component.loadApps).toHaveBeenCalled() + }) + + it('should search microfrontends on loadApps', () => { + const searchSpy = spyOn((component as any).appApi, 'searchMicrofrontends').and.returnValue( + of({ + totalElements: 0, + number: 0, + size: 0, + totalPages: 0, + stream: [] + }) + ) + + component.loadApps() + + expect(searchSpy).toHaveBeenCalledWith({ + microfrontendSearchCriteria: { productName: component.product?.name, pageSize: 1000 } + }) + }) + + it('should set correct value onLayoutChange', () => { + const viewMode = 'EDIT' + + component.onLayoutChange(viewMode) + + expect(component.viewMode).toEqual('EDIT') + }) + + it('should set correct values onFilterChange', () => { + const filter = 'filter' + + component.onFilterChange(filter) + + expect(component.filter).toEqual(filter) + }) + + it('should set correct value onSortChange', () => { + const sortField = 'field' + + component.onSortChange(sortField) + + expect(component.sortField).toEqual(sortField) + }) + + it('should set correct value onSortDirChange', () => { + let asc = true + component.onSortDirChange(asc) + expect(component.sortOrder).toEqual(-1) + + asc = false + component.onSortDirChange(asc) + expect(component.sortOrder).toEqual(1) + }) + + it('should behave correctly onDetail', () => { + const mockEvent = { stopPropagation: jasmine.createSpy() } + + component.onDetail(mockEvent, mockApp) + + expect(component.app).toEqual(mockApp) + expect(component.changeMode).toEqual('EDIT') + expect(component.displayDetailDialog).toBeTrue() + }) + + it('should behave correctly onCopy', () => { + const mockEvent = { stopPropagation: jasmine.createSpy() } + + component.onCopy(mockEvent, mockApp) + + expect(component.app).toEqual(mockApp) + expect(component.changeMode).toEqual('COPY') + expect(component.displayDetailDialog).toBeTrue() + }) + + it('should should behave correctly onCreate', () => { + component.onCreate() + + expect(component.changeMode).toEqual('CREATE') + expect(component.app).toBeUndefined() + expect(component.displayDetailDialog).toBeTrue() + }) + + it('should behave correctly onDelete', () => { + const mockEvent = { stopPropagation: jasmine.createSpy() } + + component.onDelete(mockEvent, mockApp) + + expect(component.app).toEqual(mockApp) + expect(component.displayDeleteDialog).toBeTrue() + }) }) diff --git a/src/app/product-store/product-detail/product-apps/product-apps.component.ts b/src/app/product-store/product-detail/product-apps/product-apps.component.ts index 1c9a877..1b79fee 100644 --- a/src/app/product-store/product-detail/product-apps/product-apps.component.ts +++ b/src/app/product-store/product-detail/product-apps/product-apps.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnChanges, ViewChild } from '@angular/core' import { SelectItem } from 'primeng/api' import { Observable, finalize } from 'rxjs' -import { DataViewControlTranslations, PortalMessageService, UserService } from '@onecx/portal-integration-angular' +import { DataViewControlTranslations, UserService } from '@onecx/portal-integration-angular' import { Product, MicrofrontendsAPIService, @@ -40,12 +40,7 @@ export class ProductAppsComponent implements OnChanges { public dataViewControlsTranslations: DataViewControlTranslations = {} public limitText = limitText - constructor( - private icon: IconService, - private user: UserService, - private appApi: MicrofrontendsAPIService, - private msgService: PortalMessageService - ) { + constructor(private icon: IconService, private user: UserService, private appApi: MicrofrontendsAPIService) { this.hasCreatePermission = this.user.hasPermission('MICROFRONTEND#CREATE') this.hasDeletePermission = this.user.hasPermission('MICROFRONTEND#DELETE') this.iconItems.push(...this.icon.icons.map((i) => ({ label: i, value: i }))) diff --git a/src/app/product-store/product-detail/product-intern/product-intern.component.spec.ts b/src/app/product-store/product-detail/product-intern/product-intern.component.spec.ts new file mode 100644 index 0000000..0a06e96 --- /dev/null +++ b/src/app/product-store/product-detail/product-intern/product-intern.component.spec.ts @@ -0,0 +1,37 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { RouterTestingModule } from '@angular/router/testing' +import { TranslateTestingModule } from 'ngx-translate-testing' + +import { ProductInternComponent } from './product-intern.component' + +describe('ProductInternComponent', () => { + let component: ProductInternComponent + let fixture: ComponentFixture + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ProductInternComponent], + imports: [ + RouterTestingModule, + HttpClientTestingModule, + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(ProductInternComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/src/app/product-store/product-detail/product.detail.component.spec.ts b/src/app/product-store/product-detail/product.detail.component.spec.ts index d58506c..b0a6384 100644 --- a/src/app/product-store/product-detail/product.detail.component.spec.ts +++ b/src/app/product-store/product-detail/product.detail.component.spec.ts @@ -1,22 +1,63 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { Location } from '@angular/common' import { HttpClientTestingModule } from '@angular/common/http/testing' import { RouterTestingModule } from '@angular/router/testing' -import { of } from 'rxjs' +import { Router } from '@angular/router' +import { of, throwError } from 'rxjs' import { TranslateTestingModule } from 'ngx-translate-testing' -import { PortalMessageService } from '@onecx/portal-integration-angular' +import { PortalMessageService, ConfigurationService, UserService } from '@onecx/portal-integration-angular' import { ProductDetailComponent } from './product-detail.component' +import { ProductPropertyComponent } from './product-props/product-props.component' import { ProductsAPIService } from 'src/app/shared/generated' +const product = { + id: 'id', + name: 'name', + basePath: 'path' +} + +class MockProductPropertyComponent { + onSubmit = jasmine.createSpy('onSubmit') +} + describe('ProductDetailComponent', () => { let component: ProductDetailComponent let fixture: ComponentFixture + let router: Router + const mockChildComponent = new MockProductPropertyComponent() const apiServiceSpy = { - getProductByName: jasmine.createSpy('getProductByName').and.returnValue(of({})) + getProductByName: jasmine.createSpy('getProductByName').and.returnValue(of({})), + deleteProduct: jasmine.createSpy('deleteProduct').and.returnValue(of({})) + } + const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error']) + const configServiceSpy = { + getProperty: jasmine.createSpy('getProperty').and.returnValue('123'), + getPortal: jasmine.createSpy('getPortal').and.returnValue({ + themeId: '1234', + portalName: 'test', + baseUrl: '/', + microfrontendRegistrations: [] + }), + lang: 'en' + } + const mockUserService = { + lang$: { + getValue: jasmine.createSpy('getValue').and.returnValue('en') + }, + hasPermission: jasmine.createSpy('hasPermission').and.callFake((permissionName) => { + if (permissionName === 'MICROFRONTEND#CREATE') { + return true + } else if (permissionName === 'MICROFRONTEND#EDIT') { + return true + } else { + return false + } + }) } - const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) + const locationSpy = jasmine.createSpyObj('Location', ['back']) beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -31,7 +72,10 @@ describe('ProductDetailComponent', () => { ], providers: [ { provide: ProductsAPIService, useValue: apiServiceSpy }, - { provide: PortalMessageService, useValue: msgServiceSpy } + { provide: PortalMessageService, useValue: msgServiceSpy }, + { provide: ConfigurationService, useValue: configServiceSpy }, + { provide: UserService, useValue: mockUserService }, + { provide: Location, useValue: locationSpy } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents() @@ -40,14 +84,15 @@ describe('ProductDetailComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ProductDetailComponent) component = fixture.componentInstance + router = TestBed.inject(Router) fixture.detectChanges() }) afterEach(() => { msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() - msgServiceSpy.info.calls.reset() apiServiceSpy.getProductByName.calls.reset() + apiServiceSpy.deleteProduct.calls.reset() }) it('should create', () => { @@ -72,57 +117,53 @@ describe('ProductDetailComponent', () => { expect(component.product?.id).toEqual(p.id) }) - /* - it('should prepare action buttons callbacks on init: close', () => { - spyOn(component, 'close') - - component.ngOnInit() - - const action = component.actions[0] - action.actionCallback() - expect(component.close).toHaveBeenCalled() - }) - - it('should prepare action buttons on init: onEdit', () => { + it('should prepare action buttons on init', () => { + spyOn(component, 'onClose') spyOn(component, 'onEdit') + spyOn(component, 'onCopy') + spyOn(component, 'onCancel') + spyOn(component, 'onSave') + component.product = product + component.changeMode = 'VIEW' component.ngOnInit() - const action = component.actions[1] - action.actionCallback() + let actions: any = [] + component.actions$!.subscribe((act) => (actions = act)) + + actions[0].actionCallback() + actions[1].actionCallback() + actions[2].actionCallback() + actions[3].actionCallback() + actions[4].actionCallback() + actions[5].actionCallback() + expect(component.onClose).toHaveBeenCalled() expect(component.onEdit).toHaveBeenCalled() + expect(component.onCopy).toHaveBeenCalled() + expect(component.onCancel).toHaveBeenCalled() + expect(component.onSave).toHaveBeenCalled() }) - it('should prepare action buttons on init: onCancel', () => { - spyOn(component, 'onCancel') - - component.ngOnInit() + it('should call close() onClose', () => { + spyOn(component, 'close') - const action = component.actions[2] - action.actionCallback() + component.onClose() - expect(component.onCancel).toHaveBeenCalled() + expect(component.close).toHaveBeenCalled() }) - it('should prepare action buttons on init: onSave', () => { - spyOn(component, 'onSave') - - component.ngOnInit() + it('should navigate back when closing', () => { + component.close() - const action = component.actions[3] - action.actionCallback() - - expect(component.onSave).toHaveBeenCalled() + expect(locationSpy.back).toHaveBeenCalled() }) -*/ - it('should call close() onClose', () => { - spyOn(component, 'close') - component.onClose() + it('should behave correctly onCopy', () => { + component.onCopy() - expect(component.close).toHaveBeenCalled() + expect(component.changeMode).toEqual('COPY') }) it('should behave correctly onEdit', () => { @@ -153,22 +194,32 @@ describe('ProductDetailComponent', () => { expect(component.close).toHaveBeenCalled() }) - xit('should behave correctly onSave', () => { - spyOn(component.productPropsComponent, 'onSubmit') + it('should behave correctly onCancel in copy mode', () => { + spyOn(component, 'close') + component.changeMode = 'COPY' + + component.onCancel() + + expect(component.close).toHaveBeenCalled() + }) + + it('should behave correctly onSave', () => { + component.productPropsComponent = mockChildComponent as unknown as ProductPropertyComponent component.onSave() expect(component.productPropsComponent.onSubmit).toHaveBeenCalled() }) - /* + it('should behave correctly onCreate', () => { - const data: any = { id: 'id ', name: 'name' } + const routerSpy = spyOn(router, 'navigate') - component.onCreate(data) + component.onCreate(product) - expect(component.product).toEqual(data) + expect(component.product).toEqual(product) + expect(routerSpy).toHaveBeenCalledWith(['./../', product.name], jasmine.any(Object)) }) -*/ + it('should behave correctly onNameChange if change true', () => { spyOn(component, 'close') @@ -184,4 +235,39 @@ describe('ProductDetailComponent', () => { expect(component.getProduct).toHaveBeenCalled() }) + + it('should behave correctly onDelete', () => { + const event: MouseEvent = new MouseEvent('type') + + component.onDelete(event, product) + + expect(component.product).toEqual(product) + }) + + it('should delete a product', () => { + apiServiceSpy.deleteProduct + component.product = product + + component.onDeleteConfirmation() + + expect(component.product).toBeUndefined() + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.PRODUCT.OK' }) + }) + + it('should display error message when delete fails', () => { + apiServiceSpy.deleteProduct.and.returnValue(throwError(() => new Error())) + component.product = product + + component.onDeleteConfirmation() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.PRODUCT.NOK' }) + }) + + it('should call this.user.lang$ from the constructor and set this.dateFormat to default format if user.lang$ is de', () => { + mockUserService.lang$.getValue.and.returnValue('de') + fixture = TestBed.createComponent(ProductDetailComponent) + component = fixture.componentInstance + fixture.detectChanges() + expect(component.dateFormat).toEqual('dd.MM.yyyy HH:mm:ss') + }) }) diff --git a/src/app/product-store/product-search/product-search.component.spec.ts_nok b/src/app/product-store/product-search/product-search.component.spec.ts similarity index 74% rename from src/app/product-store/product-search/product-search.component.spec.ts_nok rename to src/app/product-store/product-search/product-search.component.spec.ts index 62409db..38edbe8 100644 --- a/src/app/product-store/product-search/product-search.component.spec.ts_nok +++ b/src/app/product-store/product-search/product-search.component.spec.ts @@ -2,23 +2,18 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' import { HttpClientTestingModule } from '@angular/common/http/testing' import { RouterTestingModule } from '@angular/router/testing' -import { Router, ActivatedRoute } from '@angular/router' +import { Router } from '@angular/router' import { of } from 'rxjs' import { DataViewModule } from 'primeng/dataview' import { TranslateTestingModule } from 'ngx-translate-testing' -import { PortalMessageService } from '@onecx/portal-integration-angular' -import { ProductsAPIService } from 'src/app/shared/generated' import { ProductSearchComponent } from './product-search.component' describe('ProductSearchComponent', () => { let component: ProductSearchComponent let fixture: ComponentFixture let router: Router - let routeSpy: jasmine.SpyObj - const productApiSpy = jasmine.createSpyObj('ProductsAPIService', ['searchProducts']) - const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['get']) beforeEach(waitForAsync(() => { @@ -33,7 +28,6 @@ describe('ProductSearchComponent', () => { en: require('src/assets/i18n/en.json') }).withDefaultLanguage('en') ], - providers: [{ provide: ProductsAPIService, useValue: productApiSpy }], schemas: [NO_ERRORS_SCHEMA] }).compileComponents() })) @@ -45,25 +39,23 @@ describe('ProductSearchComponent', () => { fixture.detectChanges() }) - afterEach(() => { - msgServiceSpy.success.calls.reset() - msgServiceSpy.error.calls.reset() - msgServiceSpy.info.calls.reset() - }) - it('should create', () => { expect(component).toBeTruthy() }) it('should prepare action buttons on init', () => { - translateServiceSpy.get.and.returnValue(of({ 'ACTIONS.CREATE.LABEL': 'Create' })) + spyOn(component, 'onAppSearch') spyOn(component, 'onNewProduct') component.ngOnInit() - const action = component.actions[0] - action.actionCallback() + let actions: any = [] + component.actions$!.subscribe((act) => (actions = act)) + + actions[0].actionCallback() + actions[1].actionCallback() + expect(component.onAppSearch).toHaveBeenCalled() expect(component.onNewProduct).toHaveBeenCalled() }) @@ -92,11 +84,13 @@ describe('ProductSearchComponent', () => { }) it('should set correct value onSortDirChange', () => { - const asc = true - + let asc = true component.onSortDirChange(asc) - expect(component.sortOrder).toEqual(-1) + + asc = false + component.onSortDirChange(asc) + expect(component.sortOrder).toEqual(1) }) it('should call loadProducts onSearch', () => { @@ -116,11 +110,19 @@ describe('ProductSearchComponent', () => { expect(component.productSearchCriteriaGroup.reset).toHaveBeenCalled() }) - it('should navigate to new product on onNewProduct', () => { + it('should navigate to new product onNewProduct', () => { const routerSpy = spyOn(router, 'navigate') component.onNewProduct() - expect(routerSpy).toHaveBeenCalledWith(['./new'], { relativeTo: routeSpy }) + expect(routerSpy).toHaveBeenCalledWith(['./new'], jasmine.any(Object)) + }) + + it('should navigate to apps onAppSearch', () => { + const routerSpy = spyOn(router, 'navigate') + + component.onAppSearch() + + expect(routerSpy).toHaveBeenCalledWith(['./apps'], jasmine.any(Object)) }) }) diff --git a/src/app/product-store/product-store.module.ts b/src/app/product-store/product-store.module.ts index 7209474..bd4b000 100644 --- a/src/app/product-store/product-store.module.ts +++ b/src/app/product-store/product-store.module.ts @@ -70,6 +70,6 @@ const routes: Routes = [ }) export class ProductStoreModule { constructor() { - console.info('Theme Module constructor') + console.info('Product Store module constructor') } } diff --git a/src/app/shared/utils.spec.ts b/src/app/shared/utils.spec.ts index 6eb08cf..5586d35 100644 --- a/src/app/shared/utils.spec.ts +++ b/src/app/shared/utils.spec.ts @@ -1,8 +1,69 @@ -import { limitText } from './utils' +import { SelectItem } from 'primeng/api' + +import { limitText, dropDownSortItemsByLabel, dropDownGetLabelByValue, sortByLocale } from './utils' describe('utils', () => { - it('should limit text if text too long', () => { - const result = limitText('textData', 4) - expect(result).toBe('text...') + describe('limitText', () => { + it('should limit text if text too long', () => { + const result = limitText('textData', 4) + expect(result).toBe('text...') + }) + + it('should limit text if text too long', () => { + const result = limitText('textData', 10) + expect(result).toBe('textData') + }) + + it('return empty string if no text', () => { + const result = limitText('', 4) + expect(result).toBe('') + }) + }) + + describe('dropDownSortItemsByLabel', () => { + it('should correctly sort items by label', () => { + const items: SelectItem[] = [ + { label: 'label2', value: 2 }, + { label: 'label1', value: 1 } + ] + + const sortedItems = items.sort(dropDownSortItemsByLabel) + + expect(sortedItems[0].label).toEqual('label1') + }) + it("should treat falsy values for SelectItem.label as ''", () => { + const items: SelectItem[] = [ + { label: undefined, value: 1 }, + { label: undefined, value: 2 }, + { label: 'label1', value: 2 } + ] + + const sortedItems = items.sort(dropDownSortItemsByLabel) + + expect(sortedItems[0].label).toEqual(undefined) + }) + }) + + describe('dropDownGetLabelByValue', () => { + it('should return the label corresponding to the value', () => { + const items: SelectItem[] = [ + { label: 'label2', value: 2 }, + { label: 'label1', value: 1 } + ] + + const result = dropDownGetLabelByValue(items, '1') + + expect(result).toEqual('label1') + }) + }) + + describe('sortByLocale', () => { + it('should sort strings based on locale', () => { + const strings: string[] = ['str2', 'str1'] + + const sortedStrings = strings.sort(sortByLocale) + + expect(sortedStrings[0]).toEqual('str1') + }) }) })