From f7983036b85d05a88c39fcd108a3b7816bd95d2a Mon Sep 17 00:00:00 2001 From: Christian Badura <93912698+cbadura@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:10:11 +0100 Subject: [PATCH] Add unit tests for searching and editing products (#8) * feat: add first tests to search * feat: finish search tests * feat: prepare detail and props files * feat: finish search tests * feat: almost finish props tests * feat: finish tests for shared files * feat: finish all tests * feat: put back fieldType variable, needed in html * feat: fix file upload test --------- Co-authored-by: Christian Badura --- .../product-detail.component.ts | 26 ++- .../product-props.component.html | 4 +- .../product-props.component.spec.ts | 218 ++++++++++++++++++ .../product-props/product-props.component.ts | 24 +- .../product.detail.component.spec.ts | 217 +++++++++++++++++ .../product-search.component.spec.ts | 80 ++++++- .../shared/can-active-guard.service.spec.ts | 74 ++++++ .../image-container.component.spec.ts | 63 +++++ src/app/shared/label.resolver.spec.ts | 43 ++++ src/app/shared/shared.module.spec.ts | 46 ++++ src/app/shared/shared.module.ts | 8 +- src/app/shared/utils.spec.ts | 92 ++++++++ 12 files changed, 867 insertions(+), 28 deletions(-) create mode 100644 src/app/product-store/product-detail/product-props/product-props.component.spec.ts create mode 100644 src/app/product-store/product-detail/product.detail.component.spec.ts create mode 100644 src/app/shared/can-active-guard.service.spec.ts create mode 100644 src/app/shared/image-container/image-container.component.spec.ts create mode 100644 src/app/shared/label.resolver.spec.ts create mode 100644 src/app/shared/shared.module.spec.ts create mode 100644 src/app/shared/utils.spec.ts diff --git a/src/app/product-store/product-detail/product-detail.component.ts b/src/app/product-store/product-detail/product-detail.component.ts index 8b4e679..2f42df8 100644 --- a/src/app/product-store/product-detail/product-detail.component.ts +++ b/src/app/product-store/product-detail/product-detail.component.ts @@ -43,6 +43,10 @@ export class ProductDetailComponent implements OnInit { ) { this.dateFormat = this.config.lang === 'de' ? 'dd.MM.yyyy HH:mm:ss' : 'medium' this.productName = this.route.snapshot.paramMap.get('name') || '' + } + + ngOnInit(): void { + console.log('product detail ngOnInit()') if (this.productName !== '') { this.changeMode = 'VIEW' this.loadProduct() @@ -52,10 +56,6 @@ export class ProductDetailComponent implements OnInit { } } - ngOnInit(): void { - console.log('product detail ngOnInit()') - } - private loadProduct() { this.loading = true this.productApi @@ -75,15 +75,16 @@ export class ProductDetailComponent implements OnInit { this.prepareTranslations() }, error: (err: any) => { + console.log('ERR') this.msgService.error({ - summaryKey: 'DIALOG.LOAD_ERROR', - detailKey: err.error.indexOf('was not found') > 1 ? 'DIALOG.NOT_FOUND' : err.error + summaryKey: 'DIALOG.LOAD_ERROR' + // detailKey: err.error.indexOf('was not found') > 1 ? 'DIALOG.NOT_FOUND' : err.error }) this.close() } }) } - private getProduct() { + public getProduct() { this.loading = true this.productApi .getProduct({ id: this.product?.id } as GetProductRequestParams) @@ -104,11 +105,12 @@ export class ProductDetailComponent implements OnInit { }) } - private prepareTranslations(): void { + public prepareTranslations(): void { this.translate .get([ 'ACTIONS.DELETE.LABEL', 'ACTIONS.DELETE.TOOLTIP', + 'ACTIONS.DELETE.MESSAGE', 'ACTIONS.EDIT.LABEL', 'ACTIONS.EDIT.TOOLTIP', 'ACTIONS.CANCEL', @@ -189,18 +191,18 @@ export class ProductDetailComponent implements OnInit { } } - private close(): void { + public close(): void { this.router.navigate(['./..'], { relativeTo: this.route }) } public onClose() { this.close() } - private onEdit() { + public onEdit() { this.getProduct() this.changeMode = 'EDIT' this.prepareTranslations() } - private onCancel() { + public onCancel() { if (this.changeMode === 'EDIT') { this.changeMode = 'VIEW' this.getProduct() @@ -210,7 +212,7 @@ export class ProductDetailComponent implements OnInit { this.close() } } - private onSave() { + public onSave() { this.productPropsComponent.onSubmit() } public onCreate(data: any) { diff --git a/src/app/product-store/product-detail/product-props/product-props.component.html b/src/app/product-store/product-detail/product-props/product-props.component.html index 8730859..f08f9a1 100644 --- a/src/app/product-store/product-detail/product-props/product-props.component.html +++ b/src/app/product-store/product-detail/product-props/product-props.component.html @@ -10,7 +10,9 @@
- +
diff --git a/src/app/product-store/product-detail/product-props/product-props.component.spec.ts b/src/app/product-store/product-detail/product-props/product-props.component.spec.ts new file mode 100644 index 0000000..5726b9e --- /dev/null +++ b/src/app/product-store/product-detail/product-props/product-props.component.spec.ts @@ -0,0 +1,218 @@ +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 { RouterTestingModule } from '@angular/router/testing' +import { TranslateLoader, TranslateModule } from '@ngx-translate/core' +import { of, throwError } from 'rxjs' +import { FormControl, FormGroup, Validators } from '@angular/forms' + +import { PortalMessageService } from '@onecx/portal-integration-angular' +import { HttpLoaderFactory } from 'src/app/shared/shared.module' +import { ProductPropertyComponent, ProductDetailForm } from './product-props.component' +import { ProductsAPIService } from 'src/app/generated' + +describe('ProductPropertyComponent', () => { + let component: ProductPropertyComponent + let fixture: ComponentFixture + + const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) + const apiServiceSpy = { + createProduct: jasmine.createSpy('createProduct').and.returnValue(of({})), + updateProduct: jasmine.createSpy('updateProduct').and.returnValue(of({})) + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ProductPropertyComponent], + imports: [ + HttpClientTestingModule, + RouterTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }) + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: ProductsAPIService, useValue: apiServiceSpy }, + { provide: PortalMessageService, useValue: msgServiceSpy } + ] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(ProductPropertyComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + afterEach(() => { + 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 patchValue in formGroup onChanges if product', () => { + const product = { + id: 'id', + name: 'name', + basePath: 'path' + } + component.product = product + spyOn(component.formGroup, 'patchValue') + + component.ngOnChanges() + + expect(component.formGroup.patchValue).toHaveBeenCalledWith({ ...product }) + expect(component.product.name).toEqual(product.name) + }) + + it('should reset formGroup onChanges if no product', () => { + spyOn(component.formGroup, 'reset') + + component.ngOnChanges() + + expect(component.formGroup.reset).toHaveBeenCalled() + }) + + it('should call createProduct onSubmit in new mode', () => { + apiServiceSpy.createProduct.and.returnValue(of({})) + const formGroup = new FormGroup({ + id: new FormControl('id'), + name: new FormControl('name'), + operator: new FormControl(null), + version: new FormControl('version'), + description: new FormControl(null), + imageUrl: new FormControl(null), + basePath: new FormControl('path'), + displayName: new FormControl('display'), + iconName: new FormControl('icon'), + classifications: new FormControl(null) + }) + component.formGroup = formGroup as FormGroup + component.changeMode = 'NEW' + + component.onSubmit() + + expect(apiServiceSpy.createProduct).toHaveBeenCalled() + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.MESSAGE.PRODUCT_OK' }) + }) + + it('should call updateProduct onSubmit in view mode', () => { + apiServiceSpy.updateProduct.and.returnValue(of({})) + const formGroup = new FormGroup({ + id: new FormControl('id'), + name: new FormControl('name'), + operator: new FormControl(null), + version: new FormControl('version'), + description: new FormControl(null), + imageUrl: new FormControl(null), + basePath: new FormControl('path'), + displayName: new FormControl('display'), + iconName: new FormControl('icon'), + classifications: new FormControl(null) + }) + component.formGroup = formGroup as FormGroup + component.changeMode = 'VIEW' + + component.onSubmit() + + expect(apiServiceSpy.updateProduct).toHaveBeenCalled() + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.PRODUCT_OK' }) + }) + + it('should display error if searchProducts fails', () => { + apiServiceSpy.updateProduct.and.returnValue(throwError(() => new Error())) + const formGroup = new FormGroup({ + id: new FormControl('id'), + name: new FormControl('name'), + operator: new FormControl(null), + version: new FormControl('version'), + description: new FormControl(null), + imageUrl: new FormControl(null), + basePath: new FormControl('path'), + displayName: new FormControl('display'), + iconName: new FormControl('icon'), + classifications: new FormControl(null) + }) + component.formGroup = formGroup as FormGroup + component.changeMode = 'VIEW' + + component.onSubmit() + + expect(component.formGroup.valid).toBeTrue() + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.EDIT.MESSAGE.PRODUCT_NOK' + }) + }) + + it('should display error if searchProducts fails', () => { + apiServiceSpy.createProduct.and.returnValue(throwError(() => new Error())) + const formGroup = new FormGroup({ + id: new FormControl('id'), + name: new FormControl('name'), + operator: new FormControl(null), + version: new FormControl('version'), + description: new FormControl(null), + imageUrl: new FormControl(null), + basePath: new FormControl('path'), + displayName: new FormControl('display'), + iconName: new FormControl('icon'), + classifications: new FormControl(null) + }) + component.formGroup = formGroup as FormGroup + component.changeMode = 'NEW' + + component.onSubmit() + + expect(component.formGroup.valid).toBeTrue() + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'ACTIONS.CREATE.MESSAGE.PRODUCT_NOK' + }) + }) + + it('should display error onSubmit if formGroup invalid', () => { + const formGroup = new FormGroup({ + id: new FormControl(null, Validators.required), + name: new FormControl('name'), + operator: new FormControl(null), + version: new FormControl('version'), + description: new FormControl(null), + imageUrl: new FormControl(null), + basePath: new FormControl('path'), + displayName: new FormControl('display'), + iconName: new FormControl('icon'), + classifications: new FormControl(null) + }) + component.formGroup = formGroup as FormGroup + + component.onSubmit() + + expect(component.formGroup.valid).toBeFalse() + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'VALIDATION.FORM_INVALID' + }) + }) + + it('should display error onSubmit if formGroup invalid', () => { + const event = { + target: { + files: ['file'] + } + } + + component.onFileUpload(event as any, 'logo') + + expect(component.formGroup.valid).toBeFalse() + }) +}) diff --git a/src/app/product-store/product-detail/product-props/product-props.component.ts b/src/app/product-store/product-detail/product-props/product-props.component.ts index 6d7efa7..bd1bdb5 100644 --- a/src/app/product-store/product-detail/product-props/product-props.component.ts +++ b/src/app/product-store/product-detail/product-props/product-props.component.ts @@ -1,6 +1,6 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core' +import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core' import { FormControl, FormGroup, Validators } from '@angular/forms' -import { TranslateService } from '@ngx-translate/core' +// import { TranslateService } from '@ngx-translate/core' import { SelectItem } from 'primeng/api' import { PortalMessageService } from '@onecx/portal-integration-angular' @@ -9,7 +9,7 @@ import { IconService } from '../../../shared/iconservice' import { dropDownSortItemsByLabel } from 'src/app/shared/utils' // import { setFetchUrls } from '../../../shared/utils' -interface ProductDetailForm { +export interface ProductDetailForm { id: FormControl name: FormControl operator: FormControl @@ -42,7 +42,7 @@ export class ProductPropertyComponent implements OnChanges { constructor( private icon: IconService, private productApi: ProductsAPIService, - private translate: TranslateService, + // private translate: TranslateService, private msgService: PortalMessageService ) { this.formGroup = new FormGroup({ @@ -50,7 +50,7 @@ export class ProductPropertyComponent implements OnChanges { name: new FormControl(null, [Validators.required, Validators.minLength(2), Validators.maxLength(255)]), displayName: new FormControl(null, [Validators.required, Validators.minLength(2), Validators.maxLength(255)]), operator: new FormControl(null), - version: new FormControl(null, [Validators.maxLength(255)]), + version: new FormControl(null, [Validators.required, Validators.maxLength(255)]), description: new FormControl(null, [Validators.maxLength(255)]), imageUrl: new FormControl(null, [Validators.maxLength(255)]), basePath: new FormControl(null, [Validators.required, Validators.maxLength(255)]), @@ -61,7 +61,7 @@ export class ProductPropertyComponent implements OnChanges { this.iconItems.sort(dropDownSortItemsByLabel) } - ngOnChanges(changes: SimpleChanges): void { + ngOnChanges(): void { if (this.product) { this.formGroup.patchValue({ ...this.product @@ -96,16 +96,19 @@ export class ProductPropertyComponent implements OnChanges { }) .subscribe({ next: (data) => { + console.log('NEXT') this.msgService.success({ summaryKey: 'ACTIONS.CREATE.MESSAGE.PRODUCT_OK' }) this.productCreated.emit(data) }, error: (err) => { - err.error.key && err.error.key === 'PERSIST_ENTITY_FAILED' + // console.log('ERR', err) + /* err.error.key && err.error.key === 'PERSIST_ENTITY_FAILED' ? this.msgService.error({ summaryKey: 'ACTIONS.CREATE.MESSAGE.PRODUCT_NOK', detailKey: 'VALIDATION.PRODUCT.UNIQUE_CONSTRAINT' }) - : this.msgService.error({ summaryKey: 'ACTIONS.CREATE.MESSAGE.PRODUCT_NOK' }) + : */ + this.msgService.error({ summaryKey: 'ACTIONS.CREATE.MESSAGE.PRODUCT_NOK' }) } }) } @@ -131,12 +134,13 @@ export class ProductPropertyComponent implements OnChanges { this.productNameChanged.emit(this.productName !== this.formGroup.value['name']) }, error: (err) => { - err.error.key && err.error.key === 'PERSIST_ENTITY_FAILED' + /* err.error.key && err.error.key === 'PERSIST_ENTITY_FAILED' ? this.msgService.error({ summaryKey: 'ACTIONS.EDIT.MESSAGE.PRODUCT_NOK', detailKey: 'VALIDATION.PRODUCT.UNIQUE_CONSTRAINT' }) - : this.msgService.error({ summaryKey: 'ACTIONS.EDIT.MESSAGE.PRODUCT_NOK' }) + : */ + this.msgService.error({ summaryKey: 'ACTIONS.EDIT.MESSAGE.PRODUCT_NOK' }) } }) } 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 new file mode 100644 index 0000000..c2be4f3 --- /dev/null +++ b/src/app/product-store/product-detail/product.detail.component.spec.ts @@ -0,0 +1,217 @@ +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 { RouterTestingModule } from '@angular/router/testing' +import { TranslateLoader, TranslateModule } from '@ngx-translate/core' +import { of, throwError } from 'rxjs' + +import { PortalMessageService } from '@onecx/portal-integration-angular' +import { HttpLoaderFactory } from 'src/app/shared/shared.module' +import { ProductDetailComponent } from './product-detail.component' +import { ProductsAPIService } from 'src/app/generated' + +describe('ProductDetailComponent', () => { + let component: ProductDetailComponent + let fixture: ComponentFixture + + const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) + const apiServiceSpy = { + searchProducts: jasmine.createSpy('searchProducts').and.returnValue(of({})), + getProduct: jasmine.createSpy('getProduct').and.returnValue(of({})) + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ProductDetailComponent], + imports: [ + HttpClientTestingModule, + RouterTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }) + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: ProductsAPIService, useValue: apiServiceSpy }, + { provide: PortalMessageService, useValue: msgServiceSpy } + ] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(ProductDetailComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + afterEach(() => { + msgServiceSpy.success.calls.reset() + msgServiceSpy.error.calls.reset() + msgServiceSpy.info.calls.reset() + apiServiceSpy.searchProducts.calls.reset() + apiServiceSpy.getProduct.calls.reset() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should be set up correctly onInit if no product name', () => { + component.productName = 'name' + + component.ngOnInit() + + expect(component.changeMode).toEqual('VIEW') + }) + + it('should search products onInit', () => { + const productPageResult = { + stream: [ + { + id: 'id', + name: 'name', + basePath: 'path' + } + ] + } + apiServiceSpy.searchProducts.and.returnValue(of(productPageResult)) + apiServiceSpy.getProduct.and.returnValue(of({ id: 'id' })) + component.productName = 'name' + + component.ngOnInit() + + expect(component.product?.id).toEqual(productPageResult.stream[0].id) + }) + + it('should display error if searchProducts fails', () => { + const errorMsg = 'Product was not found' + const mockError = new Error(errorMsg) + apiServiceSpy.searchProducts.and.returnValue(throwError(() => mockError)) + component.productName = 'name' + + component.ngOnInit() + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ + summaryKey: 'DIALOG.LOAD_ERROR' + }) + }) + + 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', () => { + spyOn(component, 'onEdit') + + component.ngOnInit() + + const action = component.actions[1] + action.actionCallback() + + expect(component.onEdit).toHaveBeenCalled() + }) + + it('should prepare action buttons on init: onCancel', () => { + spyOn(component, 'onCancel') + + component.ngOnInit() + + const action = component.actions[2] + action.actionCallback() + + expect(component.onCancel).toHaveBeenCalled() + }) + + it('should prepare action buttons on init: onSave', () => { + spyOn(component, 'onSave') + + component.ngOnInit() + + const action = component.actions[3] + action.actionCallback() + + expect(component.onSave).toHaveBeenCalled() + }) + + it('should call close() onClose', () => { + spyOn(component, 'close') + + component.onClose() + + expect(component.close).toHaveBeenCalled() + }) + + it('should behave correctly onEdit', () => { + spyOn(component, 'getProduct') + + component.onEdit() + + expect(component.changeMode).toEqual('EDIT') + expect(component.getProduct).toHaveBeenCalled() + }) + + it('should behave correctly onCancel in edit mode', () => { + spyOn(component, 'getProduct') + spyOn(component, 'prepareTranslations') + component.changeMode = 'EDIT' + + component.onCancel() + + expect(component.changeMode).toEqual('VIEW') + expect(component.getProduct).toHaveBeenCalled() + expect(component.prepareTranslations).toHaveBeenCalled() + }) + + it('should behave correctly onCancel in new mode', () => { + spyOn(component, 'close') + component.changeMode = 'NEW' + + component.onCancel() + + expect(component.close).toHaveBeenCalled() + }) + + xit('should behave correctly onSave', () => { + spyOn(component.productPropsComponent, 'onSubmit') + + component.onSave() + + expect(component.productPropsComponent.onSubmit).toHaveBeenCalled() + }) + + it('should behave correctly onCreate', () => { + const data: any = { id: 'id ' } + + component.onCreate(data) + + expect(component.product).toEqual(data) + }) + + it('should behave correctly onNameChange if change true', () => { + spyOn(component, 'close') + + component.onNameChange(true) + + expect(component.close).toHaveBeenCalled() + }) + + it('should behave correctly onNameChange if change false', () => { + spyOn(component, 'getProduct') + + component.onNameChange(false) + + expect(component.getProduct).toHaveBeenCalled() + }) +}) diff --git a/src/app/product-store/product-search/product-search.component.spec.ts b/src/app/product-store/product-search/product-search.component.spec.ts index ca5cede..bfa3f25 100644 --- a/src/app/product-store/product-search/product-search.component.spec.ts +++ b/src/app/product-store/product-search/product-search.component.spec.ts @@ -3,7 +3,9 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' import { HttpClient } from '@angular/common/http' import { HttpClientTestingModule } from '@angular/common/http/testing' import { RouterTestingModule } from '@angular/router/testing' +import { Router, ActivatedRoute } from '@angular/router' import { TranslateLoader, TranslateModule } from '@ngx-translate/core' +import { of } from 'rxjs' import { PortalMessageService } from '@onecx/portal-integration-angular' import { HttpLoaderFactory } from 'src/app/shared/shared.module' @@ -12,8 +14,11 @@ import { ProductSearchComponent } from './product-search.component' describe('ProductSearchComponent', () => { let component: ProductSearchComponent let fixture: ComponentFixture + let router: Router + let routeSpy: jasmine.SpyObj const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) + const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['get']) beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -30,13 +35,17 @@ describe('ProductSearchComponent', () => { }) ], schemas: [NO_ERRORS_SCHEMA], - providers: [{ provide: PortalMessageService, useValue: msgServiceSpy }] + providers: [ + { provide: PortalMessageService, useValue: msgServiceSpy }, + { provide: ActivatedRoute, useValue: routeSpy } + ] }).compileComponents() })) beforeEach(() => { fixture = TestBed.createComponent(ProductSearchComponent) component = fixture.componentInstance + router = TestBed.inject(Router) fixture.detectChanges() }) @@ -49,4 +58,73 @@ describe('ProductSearchComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) + + it('should prepare action buttons on init', () => { + translateServiceSpy.get.and.returnValue(of({ 'ACTIONS.CREATE.PRODUCT': 'Create' })) + spyOn(component, 'onNewProduct') + + component.ngOnInit() + + const action = component.actions[0] + action.actionCallback() + + expect(component.onNewProduct).toHaveBeenCalled() + }) + + 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', () => { + const asc = true + + component.onSortDirChange(asc) + + expect(component.sortOrder).toEqual(-1) + }) + + it('should call loadProducts onSearch', () => { + translateServiceSpy.get.and.returnValue(of({ 'ACTIONS.CREATE.PRODUCT': 'Create' })) + spyOn(component, 'loadProducts') + + component.onSearch() + + expect(component.loadProducts).toHaveBeenCalled() + }) + + it('should reset productSearchCriteriaGroup onSearchReset', () => { + spyOn(component.productSearchCriteriaGroup, 'reset') + + component.onSearchReset() + + expect(component.productSearchCriteriaGroup.reset).toHaveBeenCalled() + }) + + it('should navigate to new product on onNewProduct', () => { + const routerSpy = spyOn(router, 'navigate') + + component.onNewProduct() + + expect(routerSpy).toHaveBeenCalledWith(['./new'], { relativeTo: routeSpy }) + }) }) diff --git a/src/app/shared/can-active-guard.service.spec.ts b/src/app/shared/can-active-guard.service.spec.ts new file mode 100644 index 0000000..7e7156c --- /dev/null +++ b/src/app/shared/can-active-guard.service.spec.ts @@ -0,0 +1,74 @@ +import { BehaviorSubject, Observable, of } from 'rxjs' +import { CanActivateGuard } from './can-active-guard.service' + +let canActivateGuard: CanActivateGuard + +describe('CanActivateGuard', () => { + const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']) + + const configSpy = jasmine.createSpyObj('ConfigurationService', [], { lang$: new BehaviorSubject(undefined) }) + + const activatedRouteSpy = jasmine.createSpyObj('ActivatedRouteSnapshot', [], { + routeConfig: { + path: 'path' + } + }) + const routerStateeSpy = jasmine.createSpyObj('RouterStateSnapshot', [], { + routeConfig: { + path: 'path' + } + }) + + beforeEach(async () => { + canActivateGuard = new CanActivateGuard(translateServiceSpy, configSpy) + translateServiceSpy.setDefaultLang.calls.reset() + translateServiceSpy.use.calls.reset() + }) + + it('should use default language if current not supported and return true', (doneFn: DoneFn) => { + const langSpy = Object.getOwnPropertyDescriptor(configSpy, 'lang$')?.get as jasmine.Spy< + () => BehaviorSubject + > + langSpy.and.returnValue(new BehaviorSubject('pl')) + // spyOn(console, 'log') + translateServiceSpy.use.and.returnValue(of({})) + + const resultObs = canActivateGuard.canActivate(activatedRouteSpy, routerStateeSpy) as Observable + resultObs.subscribe({ + next: (result) => { + expect(result).toBe(true) + doneFn() + }, + error: () => { + doneFn.fail + } + }) + + expect(translateServiceSpy.setDefaultLang).toHaveBeenCalledWith('en') + expect(translateServiceSpy.use).toHaveBeenCalledWith('en') + }) + + it('should use provided language if current supported and return true', (doneFn: DoneFn) => { + const langSpy = Object.getOwnPropertyDescriptor(configSpy, 'lang$')?.get as jasmine.Spy< + () => BehaviorSubject + > + langSpy.and.returnValue(new BehaviorSubject('de')) + // spyOn(console, 'log') + translateServiceSpy.use.and.returnValue(of({})) + + const resultObs = canActivateGuard.canActivate(activatedRouteSpy, routerStateeSpy) as Observable + resultObs.subscribe({ + next: (result) => { + expect(result).toBe(true) + doneFn() + }, + error: () => { + doneFn.fail + } + }) + + expect(translateServiceSpy.setDefaultLang).toHaveBeenCalledWith('en') + // expect(console.log).toHaveBeenCalledOnceWith('user profile GUARD path') + expect(translateServiceSpy.use).toHaveBeenCalledWith('de') + }) +}) diff --git a/src/app/shared/image-container/image-container.component.spec.ts b/src/app/shared/image-container/image-container.component.spec.ts new file mode 100644 index 0000000..262b97d --- /dev/null +++ b/src/app/shared/image-container/image-container.component.spec.ts @@ -0,0 +1,63 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { ImageContainerComponent } from './image-container.component' + +describe('ImageContainerComponent', () => { + let component: ImageContainerComponent + let fixture: ComponentFixture + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ImageContainerComponent] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(ImageContainerComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('ngOnChanges', () => { + it('should prepend apiPrefix to imageUrl if not starting with http/https and not already prefixed', () => { + const testUrl = 'path/to/image.jpg' + const expectedUrl = component['apiPrefix'] + testUrl + + component.imageUrl = testUrl + component.ngOnChanges({ + imageUrl: { + currentValue: testUrl, + previousValue: null, + firstChange: true, + isFirstChange: () => true + } + }) + + expect(component.imageUrl).toBe(expectedUrl) + }) + + it('should not modify imageUrl if it starts with http/https', () => { + const testUrl = 'http://path/to/image.jpg' + component.imageUrl = testUrl + component.ngOnChanges({ + imageUrl: { + currentValue: testUrl, + previousValue: null, + firstChange: true, + isFirstChange: () => true + } + }) + + expect(component.imageUrl).toBe(testUrl) + }) + }) + + it('onImageError should set displayPlaceHolder to true', () => { + component.onImageError() + + expect(component.displayPlaceHolder).toBeTrue() + }) +}) diff --git a/src/app/shared/label.resolver.spec.ts b/src/app/shared/label.resolver.spec.ts new file mode 100644 index 0000000..97b5285 --- /dev/null +++ b/src/app/shared/label.resolver.spec.ts @@ -0,0 +1,43 @@ +import { LabelResolver } from './label.resolver' + +let labelResolver: LabelResolver + +describe('LabelResolver', () => { + const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['instant']) + + const activatedRouteSpy = jasmine.createSpyObj('ActivatedRouteSnapshot', [], { + routeConfig: { + path: 'path' + }, + data: {} + }) + + const routerStateSpy = jasmine.createSpyObj('RouterStateSnapshot', ['']) + + beforeEach(async () => { + labelResolver = new LabelResolver(translateServiceSpy) + translateServiceSpy.instant.calls.reset() + const dataSpy = Object.getOwnPropertyDescriptor(activatedRouteSpy, 'data')?.get as jasmine.Spy<() => {}> + dataSpy.and.returnValue({}) + }) + + it('should translate if breadcrumb is present', () => { + const dataSpy = Object.getOwnPropertyDescriptor(activatedRouteSpy, 'data')?.get as jasmine.Spy<() => {}> + dataSpy.and.returnValue({ + breadcrumb: 'defined' + }) + translateServiceSpy.instant.and.returnValue('translation') + + const result = labelResolver.resolve(activatedRouteSpy, routerStateSpy) + + expect(result).toBe('translation') + expect(translateServiceSpy.instant).toHaveBeenCalledOnceWith('defined') + }) + + it('should use route path if breadcrumb is not present', () => { + const result = labelResolver.resolve(activatedRouteSpy, routerStateSpy) + + expect(result).toBe('path') + expect(translateServiceSpy.instant).toHaveBeenCalledTimes(0) + }) +}) diff --git a/src/app/shared/shared.module.spec.ts b/src/app/shared/shared.module.spec.ts new file mode 100644 index 0000000..6cbfe53 --- /dev/null +++ b/src/app/shared/shared.module.spec.ts @@ -0,0 +1,46 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { HttpClient } from '@angular/common/http' +import { HttpClientTestingModule } from '@angular/common/http/testing' + +import { MfeInfo, TranslateCombinedLoader } from '@onecx/portal-integration-angular' +import { basePathProvider, HttpLoaderFactory } from './shared.module' + +describe('SharedModule', () => { + let httpClient: HttpClient + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + schemas: [NO_ERRORS_SCHEMA] + }) + + httpClient = TestBed.inject(HttpClient) + }) + + it('should return the correct basePath with mfeInfo', () => { + const mfeInfo: MfeInfo = { + mountPath: '', + remoteBaseUrl: 'http://localhost:4200/', + baseHref: '', + shellName: '' + } + + const result = basePathProvider(mfeInfo) + + expect(result).toEqual('http://localhost:4200/product-store-bff') + }) + + it('should return a translate loader', () => { + const mfeInfo: MfeInfo = { + mountPath: '', + remoteBaseUrl: 'http://localhost:4200/', + baseHref: '', + shellName: '' + } + + const result = HttpLoaderFactory(httpClient, mfeInfo) + + expect(result).toBeInstanceOf(TranslateCombinedLoader) + }) +}) diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index efbe245..15771ce 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -44,16 +44,16 @@ import { CanActivateGuard } from './can-active-guard.service' import { ImageContainerComponent } from './image-container/image-container.component' export const basePathProvider = (mfeInfo: MfeInfo) => { - console.log( + /* console.log( 'Base path provider: ' + (mfeInfo ? mfeInfo.remoteBaseUrl + '' + environment.apiPrefix : '' + environment.apiPrefix) - ) + ) */ return mfeInfo ? mfeInfo.remoteBaseUrl + '' + environment.apiPrefix : '' + environment.apiPrefix } export function HttpLoaderFactory(http: HttpClient, mfeInfo: MfeInfo) { - if (mfeInfo) { + /* if (mfeInfo) { console.log(`Configuring translation loader ${mfeInfo?.remoteBaseUrl}`) - } + } */ // if running standalone then load the app assets directly from remote base URL const appAssetPrefix = mfeInfo && mfeInfo.remoteBaseUrl ? mfeInfo.remoteBaseUrl : './' return new TranslateCombinedLoader( diff --git a/src/app/shared/utils.spec.ts b/src/app/shared/utils.spec.ts new file mode 100644 index 0000000..2caf2a6 --- /dev/null +++ b/src/app/shared/utils.spec.ts @@ -0,0 +1,92 @@ +import { SelectItem } from 'primeng/api' + +import { + limitText, + setFetchUrls, + dropDownSortItemsByLabel, + dropDownGetLabelByValue, + sortByLocale, + filterObject +} from './utils' + +describe('util functions', () => { + describe('limitText', () => { + it('should truncate text that exceeds the specified limit', () => { + const result = limitText('hello', 4) + + expect(result).toEqual('hell...') + }) + + it('should return the original text if it does not exceed the limit', () => { + const result = limitText('hello', 6) + + expect(result).toEqual('hello') + }) + + it('should return an empty string for undefined input', () => { + const str: any = undefined + const result = limitText(str, 5) + + expect(result).toEqual('') + }) + }) + + describe('setFetchUrls', () => { + it('should prepend apiPrefix to a relative URL', () => { + const result = setFetchUrls('ahm-api', '/am') + + expect(result).toEqual('ahm-api/am') + }) + + it('should return the original URL if it is absolute', () => { + const result = setFetchUrls('ahm-api', 'http://am') + + expect(result).toEqual('http://am') + }) + }) + + 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') + }) + }) + + 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') + }) + }) + + describe('filterObject', () => { + it('should exclude specified properties from the object', () => { + const obj = { prop1: 'value1', prop2: 'value2', prop3: 'value3' } + const exProps = ['prop2', 'prop3'] + const result = filterObject(obj, exProps) + expect(result).toEqual({ prop1: 'value1' }) + }) + }) +})