From cab4984626b4942c0d6bea83b2d6e99c4c96dcb4 Mon Sep 17 00:00:00 2001 From: markuczy <129275100+markuczy@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:58:17 +0100 Subject: [PATCH] feat: prepared theme ui tests (#20) * feat: prepared theme ui tests * fix: theme module import order --- angular.json | 7 +- .../shared/can-active-guard.service.spec.ts | 86 ++ src/app/shared/can-active-guard.service.ts | 2 +- .../image-container.component.spec.ts | 70 ++ src/app/shared/label.resolver.spec.ts | 43 + .../theme-color-box.component.spec.ts | 40 + src/app/shared/utils.spec.ts | 8 + .../theme-designer.component.spec.ts | 915 +++++++++++++++++- .../theme-designer.component.ts | 4 +- .../theme-detail.component.spec.ts | 400 +++++++- .../theme-detail/theme-detail.component.ts | 2 +- .../theme-import.component.spec.ts | 167 +++- .../theme-import/theme-import.component.ts | 16 +- .../theme-search.component.spec.ts | 149 ++- .../theme-search/theme-search.component.ts | 2 +- src/app/theme/theme-variables.ts | 2 - src/app/theme/theme.module.ts | 2 + 17 files changed, 1867 insertions(+), 48 deletions(-) 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/theme-color-box/theme-color-box.component.spec.ts create mode 100644 src/app/shared/utils.spec.ts diff --git a/angular.json b/angular.json index c9327f4..8737f1e 100644 --- a/angular.json +++ b/angular.json @@ -109,7 +109,12 @@ "assets": ["src/favicon.ico", "src/assets"], "styles": ["src/styles.scss"], "scripts": [], - "codeCoverageExclude": ["src/app/test/**", "src/app/generated/**"] + "codeCoverageExclude": [ + "**/*.module.ts", + "src/app/test/**", + "src/app/environments/**", + "src/app/generated/**" + ] } }, "lint": { 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..56705d4 --- /dev/null +++ b/src/app/shared/can-active-guard.service.spec.ts @@ -0,0 +1,86 @@ +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), + lang: 'en' + }) + + const activatedRouteSpy = jasmine.createSpyObj('ActivatedRouteSnapshot', [], { + routeConfig: { + path: 'path' + } + }) + + const routerStateSnapshotSpy = jasmine.createSpyObj('RouterStateSnapshot', ['']) + + beforeEach(async () => { + canActivateGuard = new CanActivateGuard(translateServiceSpy, configSpy) + translateServiceSpy.setDefaultLang.calls.reset() + translateServiceSpy.use.calls.reset() + }) + + it('should return default lang if provided is not supported', () => { + const result = canActivateGuard.getBestMatchLanguage('pl') + expect(result).toBe('en') + }) + + 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, routerStateSnapshotSpy) as Observable + resultObs.subscribe({ + next: (result) => { + expect(result).toBe(true) + doneFn() + }, + error: () => { + doneFn.fail + } + }) + + expect(translateServiceSpy.setDefaultLang).toHaveBeenCalledOnceWith('en') + expect(console.log).toHaveBeenCalledWith('Start Translation guard - default language en') + expect(console.log).toHaveBeenCalledWith(`Translations guard done en`) + expect(console.log).toHaveBeenCalledWith(`Configuration language: pl`) + expect(translateServiceSpy.use).toHaveBeenCalledTimes(2) + 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, routerStateSnapshotSpy) as Observable + resultObs.subscribe({ + next: (result) => { + expect(result).toBe(true) + doneFn() + }, + error: () => { + doneFn.fail + } + }) + + expect(console.log).toHaveBeenCalledWith('Start Translation guard - default language en') + expect(console.log).toHaveBeenCalledWith(`Translations guard done en`) + expect(console.log).toHaveBeenCalledWith(`Configuration language: de`) + expect(translateServiceSpy.use).toHaveBeenCalledTimes(2) + expect(translateServiceSpy.use).toHaveBeenCalledWith('en') + expect(translateServiceSpy.use).toHaveBeenCalledWith('de') + }) +}) diff --git a/src/app/shared/can-active-guard.service.ts b/src/app/shared/can-active-guard.service.ts index 2e78bf9..69df9f7 100644 --- a/src/app/shared/can-active-guard.service.ts +++ b/src/app/shared/can-active-guard.service.ts @@ -44,7 +44,7 @@ export class CanActivateGuard implements CanActivate { ) } - private getBestMatchLanguage(lang: string): string { + public getBestMatchLanguage(lang: string): string { if (SUPPORTED_LANGUAGES.includes(lang)) { return lang } else { 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..9df578c --- /dev/null +++ b/src/app/shared/image-container/image-container.component.spec.ts @@ -0,0 +1,70 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core' +import { ImageContainerComponent } from './image-container.component' +import { TranslateLoader, TranslateModule } from '@ngx-translate/core' +import { HttpLoaderFactory } from '../shared.module' +import { HttpClient } from '@angular/common/http' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { environment } from 'src/environments/environment' + +describe('ThemeColorBoxComponent', () => { + let component: ImageContainerComponent + let fixture: ComponentFixture + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ImageContainerComponent], + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }) + ], + providers: [], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(ImageContainerComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should display placeholder on image error', () => { + component.onImageError() + + expect(component.displayPlaceHolder).toBeTrue() + }) + + it('should use imageUrl on backend after change', () => { + const changes = { + imageUrl: new SimpleChange('', 'imageUrl', false) + } + + component.imageUrl = 'imageUrl' + + component.ngOnChanges(changes) + + expect(component.imageUrl).toBe(environment.apiPrefix + 'imageUrl') + }) + + it('should use image from external resource after change', () => { + const changes = { + imageUrl: new SimpleChange('', 'http://web.com/imageUrl', false) + } + + component.imageUrl = 'http://web.com/imageUrl' + component.ngOnChanges(changes) + + expect(component.imageUrl).toBe('http://web.com/imageUrl') + }) +}) 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/theme-color-box/theme-color-box.component.spec.ts b/src/app/shared/theme-color-box/theme-color-box.component.spec.ts new file mode 100644 index 0000000..65bb7da --- /dev/null +++ b/src/app/shared/theme-color-box/theme-color-box.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { ThemeColorBoxComponent } from './theme-color-box.component' +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { TranslateLoader, TranslateModule } from '@ngx-translate/core' +import { HttpLoaderFactory } from '../shared.module' +import { HttpClient } from '@angular/common/http' + +describe('ThemeColorBoxComponent', () => { + let component: ThemeColorBoxComponent + let fixture: ComponentFixture + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ThemeColorBoxComponent], + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }) + ], + providers: [], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(ThemeColorBoxComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/src/app/shared/utils.spec.ts b/src/app/shared/utils.spec.ts new file mode 100644 index 0000000..6eb08cf --- /dev/null +++ b/src/app/shared/utils.spec.ts @@ -0,0 +1,8 @@ +import { limitText } from './utils' + +describe('utils', () => { + it('should limit text if text too long', () => { + const result = limitText('textData', 4) + expect(result).toBe('text...') + }) +}) diff --git a/src/app/theme/theme-designer/theme-designer.component.spec.ts b/src/app/theme/theme-designer/theme-designer.component.spec.ts index 1d1cd07..4a06346 100644 --- a/src/app/theme/theme-designer/theme-designer.component.spec.ts +++ b/src/app/theme/theme-designer/theme-designer.component.spec.ts @@ -1,13 +1,27 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' -import { HttpClient } from '@angular/common/http' +import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing' +import { HttpClient, HttpErrorResponse } 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 { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' -import { ConfigurationService, PortalMessageService } from '@onecx/portal-integration-angular' +import { ConfigurationService, PortalMessageService, ThemeService } from '@onecx/portal-integration-angular' import { HttpLoaderFactory } from 'src/app/shared/shared.module' import { ThemeDesignerComponent } from './theme-designer.component' +import { ConfirmationService } from 'primeng/api' +import { ThemesAPIService } from 'src/app/generated' +import { ActivatedRoute, Router } from '@angular/router' +import { themeVariables } from '../theme-variables' +import { of, throwError } from 'rxjs' +import { environment } from 'src/environments/environment' +import { ConfirmDialog, ConfirmDialogModule } from 'primeng/confirmdialog' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { By } from '@angular/platform-browser' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { InputSwitchModule } from 'primeng/inputswitch' +import { DialogModule } from 'primeng/dialog' +import { DropdownModule } from 'primeng/dropdown' +import { OverlayPanelModule } from 'primeng/overlaypanel' describe('ThemeDesignerComponent', () => { let component: ThemeDesignerComponent @@ -23,6 +37,13 @@ describe('ThemeDesignerComponent', () => { microfrontendRegistrations: [] }) } + const themeServiceSpy = jasmine.createSpyObj('ThemeService', ['apply']) + const themeApiSpy = jasmine.createSpyObj('ThemesAPIService', [ + 'getThemes', + 'updateTheme', + 'createTheme', + 'getThemeById' + ]) beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -36,26 +57,892 @@ describe('ThemeDesignerComponent', () => { useFactory: HttpLoaderFactory, deps: [HttpClient] } - }) + }), + ConfirmDialogModule, + DialogModule, + DropdownModule, + OverlayPanelModule, + BrowserAnimationsModule, + ReactiveFormsModule, + FormsModule, + InputSwitchModule ], schemas: [NO_ERRORS_SCHEMA], providers: [ { provide: ConfigurationService, useValue: configServiceSpy }, - { provide: PortalMessageService, useValue: msgServiceSpy } + { provide: PortalMessageService, useValue: msgServiceSpy }, + { provide: ThemeService, useValue: themeServiceSpy }, + { provide: ThemesAPIService, useValue: themeApiSpy }, + ConfirmationService ] - }).compileComponents(), - msgServiceSpy.success.calls.reset() + }).compileComponents() + msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() msgServiceSpy.info.calls.reset() + themeApiSpy.getThemeById.calls.reset() + themeApiSpy.updateTheme.calls.reset() + themeApiSpy.createTheme.calls.reset() + themeApiSpy.getThemes.calls.reset() + themeServiceSpy.apply.calls.reset() + + themeApiSpy.getThemes.and.returnValue(of({}) as any) + themeApiSpy.updateTheme.and.returnValue(of({}) as any) + themeApiSpy.createTheme.and.returnValue(of({}) as any) + themeApiSpy.getThemeById.and.returnValue(of({}) as any) })) - beforeEach(() => { - fixture = TestBed.createComponent(ThemeDesignerComponent) - component = fixture.componentInstance - fixture.detectChanges() + describe('when constructing', () => { + beforeEach(() => {}) + + it('should have edit mode when id present in route', () => { + const activatedRoute = TestBed.inject(ActivatedRoute) + spyOn(activatedRoute.snapshot.paramMap, 'has').and.returnValue(true) + + fixture = TestBed.createComponent(ThemeDesignerComponent) + component = fixture.componentInstance + fixture.detectChanges() + + expect(component.mode).toBe('EDIT') + }) + + it('should have create mode when id not present in route', () => { + const activatedRoute = TestBed.inject(ActivatedRoute) + spyOn(activatedRoute.snapshot.paramMap, 'has').and.returnValue(false) + + fixture = TestBed.createComponent(ThemeDesignerComponent) + component = fixture.componentInstance + fixture.detectChanges() + + expect(component.mode).toBe('NEW') + }) + + it('should populate state and create forms', () => { + const activatedRoute = TestBed.inject(ActivatedRoute) + spyOn(activatedRoute.snapshot.paramMap, 'get').and.returnValue('themeId') + + fixture = TestBed.createComponent(ThemeDesignerComponent) + component = fixture.componentInstance + fixture.detectChanges() + + expect(component.themeId).toBe('themeId') + expect(component.themeIsCurrentUsedTheme).toBeFalse() + expect(Object.keys(component.fontForm.controls).length).toBe(themeVariables.font.length) + expect(Object.keys(component.generalForm.controls).length).toBe(themeVariables.general.length) + expect(Object.keys(component.topbarForm.controls).length).toBe(themeVariables.topbar.length) + expect(Object.keys(component.sidebarForm.controls).length).toBe(themeVariables.sidebar.length) + }) + + it('should load translations', () => { + const translateService = TestBed.inject(TranslateService) + const actionsTranslations = { + 'ACTIONS.CANCEL': 'actionCancel', + 'ACTIONS.TOOLTIPS.CANCEL_AND_CLOSE': 'actionTooltipsCancelAndClose', + 'ACTIONS.SAVE': 'actionsSave', + 'ACTIONS.TOOLTIPS.SAVE': 'actionsTooltipsSave', + 'ACTIONS.SAVE_AS': 'actionSaveAs', + 'ACTIONS.TOOLTIPS.SAVE_AS': 'actionTooltipsSaveAs' + } + spyOn(translateService, 'get').and.returnValue(of(actionsTranslations)) + + fixture = TestBed.createComponent(ThemeDesignerComponent) + component = fixture.componentInstance + fixture.detectChanges() + + expect(component.actions.length).toBe(3) + const cancelAction = component.actions.filter( + (a) => a.label === 'actionCancel' && a.title === 'actionTooltipsCancelAndClose' + )[0] + spyOn(component, 'close') + cancelAction.actionCallback() + expect(component['close']).toHaveBeenCalledTimes(1) + + const saveAction = component.actions.filter( + (a) => a.label === 'actionsSave' && a.title === 'actionsTooltipsSave' + )[0] + spyOn(component, 'updateTheme') + saveAction.actionCallback() + expect(component['updateTheme']).toHaveBeenCalledTimes(1) + + const saveAsAction = component.actions.filter( + (a) => a.label === 'actionSaveAs' && a.title === 'actionTooltipsSaveAs' + )[0] + spyOn(component, 'saveAsNewPopup') + saveAsAction.actionCallback() + expect(component.saveAsNewPopup).toHaveBeenCalledTimes(1) + }) + + it('should update document style on form changes', fakeAsync(() => { + fixture = TestBed.createComponent(ThemeDesignerComponent) + component = fixture.componentInstance + fixture.detectChanges() + + component.autoApply = true + + const fontFormControlEl = fixture.debugElement.query(By.css('#font-family')) + expect(fontFormControlEl).toBeDefined() + fontFormControlEl.nativeElement.value = 'newFamily' + fontFormControlEl.nativeElement.dispatchEvent(new Event('input')) + + const generalFormControlEl = fixture.debugElement.query(By.css('#color-primary-color')) + expect(generalFormControlEl).toBeDefined() + generalFormControlEl.nativeElement.value = 'rgba(0, 0, 0, 0.87)' + generalFormControlEl.nativeElement.dispatchEvent(new Event('input')) + + const topbarFormControlEl = fixture.debugElement.query(By.css('#color-topbar-bg-color')) + expect(topbarFormControlEl).toBeDefined() + topbarFormControlEl.nativeElement.value = '#000000' + topbarFormControlEl.nativeElement.dispatchEvent(new Event('input')) + + const sidebarFormControlEl = fixture.debugElement.query(By.css('#color-menu-text-color')) + expect(sidebarFormControlEl).toBeDefined() + sidebarFormControlEl.nativeElement.value = '#102030' + sidebarFormControlEl.nativeElement.dispatchEvent(new Event('input')) + + fixture.detectChanges() + tick(300) + + expect(document.documentElement.style.getPropertyValue(`--primary-color`)).toBe('rgba(0, 0, 0, 0.87)') + expect(document.documentElement.style.getPropertyValue(`--topbar-bg-color`)).toBe('#000000') + expect(document.documentElement.style.getPropertyValue(`--topbar-bg-color-rgb`)).toBe('0,0,0') + expect(document.documentElement.style.getPropertyValue(`--menu-text-color`)).toBe('#102030') + expect(document.documentElement.style.getPropertyValue(`--menu-text-color-rgb`)).toBe('16,32,48') + })) }) - it('should create', () => { - expect(component).toBeTruthy() + describe('after creation', () => { + beforeEach(() => { + fixture = TestBed.createComponent(ThemeDesignerComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should populate form with theme data in edit mode', () => { + const themeData = { + id: 'id', + description: 'desc', + logoUrl: 'logo_url', + faviconUrl: 'fav_url', + name: 'themeName', + properties: { + font: { + 'font-family': 'myFont' + }, + general: { + 'primary-color': 'rgb(0,0,0)' + } + } + } + const themeResponse = { + resource: themeData + } + themeApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + component.mode = 'EDIT' + component.themeId = 'themeId' + + component.ngOnInit() + + expect(component.theme).toBe(themeData) + expect(themeApiSpy.getThemeById).toHaveBeenCalledOnceWith({ id: 'themeId' }) + expect(component.basicForm.controls['name'].value).toBe(themeData.name) + expect(component.basicForm.controls['description'].value).toBe(themeData.description) + expect(component.basicForm.controls['logoUrl'].value).toBe(themeData.logoUrl) + expect(component.basicForm.controls['faviconUrl'].value).toBe(themeData.faviconUrl) + expect(component.fontForm.controls['font-family'].value).toBe('myFont') + expect(component.generalForm.controls['primary-color'].value).toBe('rgb(0,0,0)') + }) + + it('should fetch logo and favicon from backend on edit mode when no http[s] present', () => { + const themeData = { + name: 'themeName', + logoUrl: 'logo_url', + faviconUrl: 'fav_url' + } + const themeResponse = { + resource: themeData + } + themeApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + component.mode = 'EDIT' + component.themeId = 'themeId' + + component.ngOnInit() + + expect(component.fetchingLogoUrl).toBe(environment.apiPrefix + themeData.logoUrl) + expect(component.fetchingFaviconUrl).toBe(environment.apiPrefix + themeData.faviconUrl) + }) + + it('should fetch logo and favicon from external source on edit mode when http[s] present', () => { + const themeData = { + logoUrl: 'http://myWeb.com/logo_url', + faviconUrl: 'https://otherWeb.de/fav_url' + } + const themeResponse = { + resource: themeData + } + themeApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + component.mode = 'EDIT' + component.themeId = 'themeId' + + component.ngOnInit() + + expect(component.fetchingLogoUrl).toBe(themeData.logoUrl) + expect(component.fetchingFaviconUrl).toBe(themeData.faviconUrl) + }) + + it('should populate forms with default values if not in edit mode', () => { + const documentStyle = getComputedStyle(document.documentElement).getPropertyValue('--font-family') + + component.ngOnInit() + + expect(component.fontForm.controls['font-family'].value).toBe(documentStyle) + }) + + it('should load all templates basic data on initialization', () => { + const themeArr = [ + { + id: 'id1', + name: 'theme1', + description: 'desc1' + }, + { + id: 'id2', + name: 'myTheme', + description: 'desc2' + } + ] + themeApiSpy.getThemes.and.returnValue( + of({ + stream: themeArr + }) as any + ) + + component.ngOnInit() + expect(component.themeTemplates).toEqual([ + { + label: 'myTheme', + value: 'id2' + }, + { + label: 'theme1', + value: 'id1' + } + ]) + }) + + it('should navigate back on close', () => { + const router = TestBed.inject(Router) + spyOn(router, 'navigate') + + component.actions[0].actionCallback() + + expect(router.navigate).toHaveBeenCalledOnceWith(['./..'], jasmine.any(Object)) + }) + + it('should display error when updating theme with invalid form', () => { + spyOnProperty(component.propertiesForm, 'invalid').and.returnValue(true) + + component.actions[1].actionCallback() + + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.CHANGE_NOK' }) + }) + + it('should display error when updating theme call fails', () => { + const themeData = { + id: 'id', + description: 'desc', + logoUrl: 'logo_url', + faviconUrl: 'fav_url', + name: 'themeName', + properties: { + font: { + 'font-family': 'myFont' + }, + general: { + 'primary-color': 'rgb(0,0,0)' + } + } + } + const themeResponse = { + resource: themeData + } + themeApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + themeApiSpy.updateTheme.and.returnValue(throwError(() => new Error())) + + component.actions[1].actionCallback() + + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.CHANGE_NOK' }) + }) + + it('should only update properties and base theme data and show success when updating theme call is successful', () => { + component.themeId = 'id' + const themeData = { + id: 'id', + description: 'desc', + logoUrl: 'logo_url', + faviconUrl: 'fav_url', + name: 'themeName', + properties: { + font: { + 'font-family': 'myFont' + }, + general: { + 'primary-color': 'rgb(0,0,0)' + } + } + } + const themeResponse = { + resource: themeData + } + themeApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + + component.fontForm.patchValue({ + 'font-family': 'updatedFont' + }) + component.generalForm.patchValue({ + 'primary-color': 'rgb(255,255,255)' + }) + const newBasicData = { + name: 'updatedName', + description: 'updatedDesc', + logoUrl: 'updated_logo_url', + faviconUrl: 'updated_favicon_url' + } + component.basicForm.patchValue(newBasicData) + + themeApiSpy.updateTheme.and.returnValue(of({}) as any) + + component.actions[1].actionCallback() + expect(msgServiceSpy.success).toHaveBeenCalledOnceWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.CHANGE_OK' }) + expect(themeApiSpy.updateTheme).toHaveBeenCalledTimes(1) + const updateArgs = themeApiSpy.updateTheme.calls.mostRecent().args[0] + expect(updateArgs.updateThemeRequest?.resource.name).toBe(newBasicData.name) + expect(updateArgs.updateThemeRequest?.resource.description).toBe(newBasicData.description) + expect(updateArgs.updateThemeRequest?.resource.logoUrl).toBe(newBasicData.logoUrl) + expect(updateArgs.updateThemeRequest?.resource.faviconUrl).toBe(newBasicData.faviconUrl) + expect(updateArgs.updateThemeRequest?.resource.properties).toEqual( + jasmine.objectContaining({ + font: jasmine.objectContaining({ + 'font-family': 'updatedFont' + }), + general: jasmine.objectContaining({ + 'primary-color': 'rgb(255,255,255)' + }) + }) + ) + }) + + it('should apply changes when updating current theme is successful', () => { + component.themeId = 'id' + const themeData = { + id: 'id', + description: 'desc', + logoUrl: 'logo_url', + faviconUrl: 'fav_url', + name: 'themeName', + properties: { + font: { + 'font-family': 'myFont' + }, + general: { + 'primary-color': 'rgb(0,0,0)' + } + } + } + const themeResponse = { + resource: themeData + } + themeApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + + const updateThemeData = { + resource: { + id: 'updatedCallId' + } + } + themeApiSpy.updateTheme.and.returnValue(of(updateThemeData) as any) + + component.themeIsCurrentUsedTheme = true + + component.actions[1].actionCallback() + + expect(themeServiceSpy.apply).toHaveBeenCalledOnceWith(updateThemeData as any) + }) + + it('should display theme already exists message on theme save failure', () => { + themeApiSpy.createTheme.and.returnValue( + throwError( + () => + new HttpErrorResponse({ + error: { + key: 'PERSIST_ENTITY_FAILED' + } + }) + ) + ) + + component.saveTheme('myTheme') + + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ + summaryKey: 'ACTIONS.CREATE.MESSAGE.CREATE_NOK', + detailKey: 'ACTIONS.CREATE.MESSAGE.THEME_ALREADY_EXISTS' + }) + }) + + it('should display error message on theme save failure', () => { + const responseError = 'Error message' + themeApiSpy.createTheme.and.returnValue( + throwError( + () => + new HttpErrorResponse({ + error: responseError + }) + ) + ) + + component.saveTheme('myTheme') + + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ + summaryKey: 'ACTIONS.CREATE.MESSAGE.CREATE_NOK', + detailKey: responseError + }) + }) + + it('should display success message and route correctly in edit mode', () => { + const router = TestBed.inject(Router) + spyOn(router, 'navigate') + + const route = TestBed.inject(ActivatedRoute) + + const newBasicData = { + name: 'newName', + description: 'newDesc', + logoUrl: 'new_logo_url', + faviconUrl: 'new_favicon_url' + } + component.basicForm.patchValue(newBasicData) + component.fontForm.patchValue({ + 'font-family': 'newFont' + }) + component.generalForm.patchValue({ + 'primary-color': 'rgb(255,255,255)' + }) + themeApiSpy.createTheme.and.returnValue( + of({ + resource: { + id: 'myThemeId' + } + }) as any + ) + component.mode = 'EDIT' + + component.saveTheme('myTheme') + + const createArgs = themeApiSpy.createTheme.calls.mostRecent().args[0] + expect(createArgs.createThemeRequest?.resource).toEqual( + jasmine.objectContaining({ + name: 'myTheme', + description: newBasicData.description, + logoUrl: newBasicData.logoUrl, + faviconUrl: newBasicData.faviconUrl, + properties: jasmine.objectContaining({ + font: jasmine.objectContaining({ + 'font-family': 'newFont' + }), + general: jasmine.objectContaining({ + 'primary-color': 'rgb(255,255,255)' + }) + }) + }) + ) + expect(router.navigate).toHaveBeenCalledOnceWith( + [`../../myThemeId`], + jasmine.objectContaining({ relativeTo: route }) + ) + }) + + it('should display success message and route correctly in new mode', () => { + const router = TestBed.inject(Router) + spyOn(router, 'navigate') + + const route = TestBed.inject(ActivatedRoute) + + const newBasicData = { + name: 'newName', + description: 'newDesc', + logoUrl: 'new_logo_url', + faviconUrl: 'new_favicon_url' + } + component.basicForm.patchValue(newBasicData) + component.fontForm.patchValue({ + 'font-family': 'newFont' + }) + component.generalForm.patchValue({ + 'primary-color': 'rgb(255,255,255)' + }) + themeApiSpy.createTheme.and.returnValue( + of({ + resource: { + id: 'myThemeId' + } + }) as any + ) + component.mode = 'NEW' + + component.saveTheme('myTheme') + + expect(router.navigate).toHaveBeenCalledOnceWith( + [`../myThemeId`], + jasmine.objectContaining({ relativeTo: route }) + ) + }) + + it('should display save as new popup on save as click', () => { + component.saveAsNewPopupDisplay = false + + component.actions[2].actionCallback() + + expect(component.saveAsNewPopupDisplay).toBe(true) + }) + + it('should use form theme name in save as dialog while in NEW mode', () => { + component.saveAsThemeName = jasmine.createSpyObj('ElementRef', [], { + nativeElement: { + value: '' + } + }) + + component.basicForm.controls['name'].setValue('newThemeName') + + component.mode = 'NEW' + + component.onShowSaveAsDialog() + + expect(component.saveAsThemeName?.nativeElement.value).toBe('newThemeName') + }) + + it('should use COPY_OF + form theme name in save as dialog while in EDIT mode', () => { + const translateService = TestBed.inject(TranslateService) + spyOn(translateService, 'instant').and.returnValue('copy_of: ') + component.saveAsThemeName = jasmine.createSpyObj('ElementRef', [], { + nativeElement: { + value: '' + } + }) + + component.basicForm.controls['name'].setValue('newThemeName') + + component.mode = 'EDIT' + + component.onShowSaveAsDialog() + + expect(component.saveAsThemeName?.nativeElement.value).toBe('copy_of: newThemeName') + }) + + // it('should display logo file type error if uploaded file is not an image', () => { + // const dataTransfer = new DataTransfer() + // dataTransfer.items.add(new File([''], 'my-file.pdf')) + + // expect(component.selectedFileInputLogo).toBeDefined() + // component.selectedFileInputLogo!.nativeElement.files = dataTransfer.files + + // component.selectedFileInputLogo!.nativeElement.dispatchEvent(new InputEvent('change')) + + // fixture.detectChanges() + + // expect(component.displayFileTypeErrorLogo).toBe(true) + // expect(component.displayFileTypeErrorFavicon).toBe(false) + // }) + + // it('should display favicon file type error if uploaded file is not an image', () => { + // const dataTransfer = new DataTransfer() + // dataTransfer.items.add(new File([''], 'my-file.pdf')) + + // expect(component.selectedFileInputFavicon).toBeDefined() + // component.selectedFileInputFavicon!.nativeElement.files = dataTransfer.files + + // component.selectedFileInputFavicon!.nativeElement.dispatchEvent(new InputEvent('change')) + + // fixture.detectChanges() + + // expect(component.displayFileTypeErrorLogo).toBe(false) + // expect(component.displayFileTypeErrorFavicon).toBe(true) + // }) + + // it('should upload logo image, update logo urls and display message on logo upload', () => { + // imageApiSpy.uploadImage.and.returnValue( + // of({ + // imageUrl: 'uploadedLogoUrl' + // }) as any + // ) + + // const dataTransfer = new DataTransfer() + // dataTransfer.items.add(new File([''], 'my-logo.png')) + + // expect(component.selectedFileInputLogo).toBeDefined() + // component.selectedFileInputLogo!.nativeElement.files = dataTransfer.files + + // component.selectedFileInputLogo!.nativeElement.dispatchEvent(new InputEvent('change')) + + // fixture.detectChanges() + + // expect(imageApiSpy.uploadImage).toHaveBeenCalledOnceWith({ + // image: dataTransfer.files[0] + // }) + // expect(component.basicForm.controls['logoUrl'].value).toBe('uploadedLogoUrl') + // expect(component.fetchingLogoUrl).toBe(environment.apiPrefix + 'uploadedLogoUrl') + // expect(msgServiceSpy.info).toHaveBeenCalledOnceWith({ summaryKey: 'LOGO.UPLOADED' }) + // }) + + // it('should upload favicon image, update favicon urls and display information on favicon upload', () => { + // imageApiSpy.uploadImage.and.returnValue( + // of({ + // imageUrl: 'uploadedFaviconUrl' + // }) as any + // ) + + // const dataTransfer = new DataTransfer() + // dataTransfer.items.add(new File([''], 'my-favicon.png')) + + // expect(component.selectedFileInputFavicon).toBeDefined() + // component.selectedFileInputFavicon!.nativeElement.files = dataTransfer.files + + // component.selectedFileInputFavicon!.nativeElement.dispatchEvent(new InputEvent('change')) + + // fixture.detectChanges() + + // expect(imageApiSpy.uploadImage).toHaveBeenCalledOnceWith({ + // image: dataTransfer.files[0] + // }) + // expect(component.basicForm.controls['faviconUrl'].value).toBe('uploadedFaviconUrl') + // expect(component.fetchingFaviconUrl).toBe(environment.apiPrefix + 'uploadedFaviconUrl') + // expect(msgServiceSpy.info).toHaveBeenCalledOnceWith({ summaryKey: 'LOGO.UPLOADED' }) + // }) + + it('should use translation data on theme template change', () => { + component.themeTemplates = [ + { + label: 'theme1', + value: 'id1' + }, + { + label: 'myTheme', + value: 'id2' + } + ] + + component.themeTemplateSelectedId = 'id2' + + const translationData = { + 'GENERAL.COPY_OF': 'generalCopyOf', + 'THEME.TEMPLATE.CONFIRMATION.HEADER': 'themeTemplateConfirmationHeader', + 'THEME.TEMPLATE.CONFIRMATION.MESSAGE': '{{ITEM}} themeTemplateConfirmationMessage', + 'ACTIONS.CONFIRMATION.YES': 'actionsConfirmationYes', + 'ACTIONS.CONFIRMATION.NO': 'actionsConfirmationNo' + } + const translateService = TestBed.inject(TranslateService) + spyOn(translateService, 'get').and.returnValue(of(translationData)) + + let confirmdialog: ConfirmDialog + confirmdialog = fixture.debugElement.query(By.css('p-confirmdialog')).componentInstance + + component.onThemeTemplateDropdownChange() + fixture.detectChanges() + + expect(confirmdialog.confirmation).toEqual( + jasmine.objectContaining({ + header: 'themeTemplateConfirmationHeader', + message: 'myTheme themeTemplateConfirmationMessage', + acceptLabel: 'actionsConfirmationYes', + rejectLabel: 'actionsConfirmationNo' + }) + ) + }) + + it('should reset selected template on confirmation reject', () => { + component.themeTemplates = [ + { + label: 'theme1', + value: 'id1' + }, + { + label: 'myTheme', + value: 'id2' + } + ] + + component.themeTemplateSelectedId = 'id2' + + let confirmdialog: ConfirmDialog + confirmdialog = fixture.debugElement.query(By.css('p-confirmdialog')).componentInstance + let reject = spyOn(confirmdialog, 'reject').and.callThrough() + + component.onThemeTemplateDropdownChange() + fixture.detectChanges() + component = fixture.componentInstance + + let cancelBtn = fixture.debugElement.nativeElement.querySelector('.p-confirm-dialog-reject') + cancelBtn.click() + + expect(reject).toHaveBeenCalled() + expect(component.themeTemplateSelectedId).toBe('') + }) + + it('should populate only properties with template data on confirmation accept and EDIT mode', () => { + component.themeTemplates = [ + { + label: 'theme1', + value: 'id1' + }, + { + label: 'myTheme', + value: 'id2' + } + ] + + component.themeTemplateSelectedId = 'id2' + + const translationData = { + 'GENERAL.COPY_OF': 'generalCopyOf', + 'THEME.TEMPLATE.CONFIRMATION.HEADER': 'themeTemplateConfirmationHeader', + 'THEME.TEMPLATE.CONFIRMATION.MESSAGE': '{{ITEM}} themeTemplateConfirmationMessage', + 'ACTIONS.CONFIRMATION.YES': 'actionsConfirmationYes', + 'ACTIONS.CONFIRMATION.NO': 'actionsConfirmationNo' + } + const translateService = TestBed.inject(TranslateService) + spyOn(translateService, 'get').and.returnValue(of(translationData)) + + component.mode = 'EDIT' + const basicFormBeforeFetch = { + name: 'n', + description: 'd', + faviconUrl: 'f', + logoUrl: 'l' + } + component.basicForm.patchValue(basicFormBeforeFetch) + + const fetchedTheme = { + name: 'fetchedName', + description: 'fetchedDesc', + faviconUrl: 'fetchedFavUrl', + logoUrl: 'fetchedLogoUrl', + properties: { + font: { + 'font-family': 'fetchedFont' + }, + general: { + 'primary-color': 'rgb(255,255,255)' + } + } + } + const fetchedThemeResponse = { + resource: fetchedTheme + } + themeApiSpy.getThemeById.and.returnValue(of(fetchedThemeResponse) as any) + + component.fetchingFaviconUrl = 'ffu' + component.fetchingLogoUrl = 'flu' + + let confirmdialog: ConfirmDialog + confirmdialog = fixture.debugElement.query(By.css('p-confirmdialog')).componentInstance + let accept = spyOn(confirmdialog, 'accept').and.callThrough() + + component.onThemeTemplateDropdownChange() + fixture.detectChanges() + + let acceptBtn = fixture.debugElement.nativeElement.querySelector('.p-confirm-dialog-accept') + acceptBtn.click() + + expect(accept).toHaveBeenCalled() + expect(component.basicForm.value).toEqual(basicFormBeforeFetch) + expect(component.propertiesForm.value).toEqual( + jasmine.objectContaining({ + font: jasmine.objectContaining({ 'font-family': 'fetchedFont' }), + general: jasmine.objectContaining({ 'primary-color': 'rgb(255,255,255)' }) + }) + ) + expect(component.fetchingFaviconUrl).toBe('ffu') + expect(component.fetchingLogoUrl).toBe('flu') + }) + + it('should populate properties and basic info with template data on confirmation accept and NEW mode', () => { + component.themeTemplates = [ + { + label: 'theme1', + value: 'id1' + }, + { + label: 'myTheme', + value: 'id2' + } + ] + + component.themeTemplateSelectedId = 'id2' + + const translationData = { + 'GENERAL.COPY_OF': 'generalCopyOf: ', + 'THEME.TEMPLATE.CONFIRMATION.HEADER': 'themeTemplateConfirmationHeader', + 'THEME.TEMPLATE.CONFIRMATION.MESSAGE': '{{ITEM}} themeTemplateConfirmationMessage', + 'ACTIONS.CONFIRMATION.YES': 'actionsConfirmationYes', + 'ACTIONS.CONFIRMATION.NO': 'actionsConfirmationNo' + } + const translateService = TestBed.inject(TranslateService) + spyOn(translateService, 'get').and.returnValue(of(translationData)) + + component.mode = 'NEW' + const basicFormBeforeFetch = { + name: 'n', + description: 'd', + faviconUrl: 'f', + logoUrl: 'l' + } + component.basicForm.patchValue(basicFormBeforeFetch) + + const fetchedTheme = { + name: 'fetchedName', + description: 'fetchedDesc', + faviconUrl: 'fetchedFavUrl', + logoUrl: 'fetchedLogoUrl', + properties: { + font: { + 'font-family': 'fetchedFont' + }, + general: { + 'primary-color': 'rgb(255,255,255)' + } + } + } + const fetchedThemeResponse = { + resource: fetchedTheme + } + themeApiSpy.getThemeById.and.returnValue(of(fetchedThemeResponse) as any) + + let confirmdialog: ConfirmDialog + confirmdialog = fixture.debugElement.query(By.css('p-confirmdialog')).componentInstance + let accept = spyOn(confirmdialog, 'accept').and.callThrough() + + component.onThemeTemplateDropdownChange() + fixture.detectChanges() + + let acceptBtn = fixture.debugElement.nativeElement.querySelector('.p-confirm-dialog-accept') + acceptBtn.click() + + expect(accept).toHaveBeenCalled() + expect(component.basicForm.value).toEqual({ + name: 'generalCopyOf: fetchedName', + description: 'fetchedDesc', + faviconUrl: 'fetchedFavUrl', + logoUrl: 'fetchedLogoUrl' + }) + expect(component.propertiesForm.value).toEqual( + jasmine.objectContaining({ + font: jasmine.objectContaining({ 'font-family': 'fetchedFont' }), + general: jasmine.objectContaining({ 'primary-color': 'rgb(255,255,255)' }) + }) + ) + expect(component.fetchingFaviconUrl).toBe(environment.apiPrefix + 'fetchedFavUrl') + expect(component.fetchingLogoUrl).toBe(environment.apiPrefix + 'fetchedLogoUrl') + }) }) }) diff --git a/src/app/theme/theme-designer/theme-designer.component.ts b/src/app/theme/theme-designer/theme-designer.component.ts index e59382d..8609cf0 100644 --- a/src/app/theme/theme-designer/theme-designer.component.ts +++ b/src/app/theme/theme-designer/theme-designer.component.ts @@ -154,7 +154,7 @@ export class ThemeDesignerComponent implements OnInit { if (this.mode === 'EDIT' && this.themeId) { this.getThemeById(this.themeId).subscribe((data) => { this.theme = data.resource - this.basicForm.patchValue(data) + this.basicForm.patchValue(data.resource) this.propertiesForm.reset() this.propertiesForm.patchValue(data.resource.properties || {}) this.setFetchUrls() @@ -289,7 +289,7 @@ export class ThemeDesignerComponent implements OnInit { .pipe( switchMap((data) => { data.resource.properties = this.propertiesForm.value - Object.assign(data, this.basicForm.value) + Object.assign(data.resource, this.basicForm.value) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.themeApi.updateTheme({ id: this.themeId!, diff --git a/src/app/theme/theme-detail/theme-detail.component.spec.ts b/src/app/theme/theme-detail/theme-detail.component.spec.ts index d84e422..90a776d 100644 --- a/src/app/theme/theme-detail/theme-detail.component.spec.ts +++ b/src/app/theme/theme-detail/theme-detail.component.spec.ts @@ -1,19 +1,41 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' -import { HttpClient } from '@angular/common/http' +import { HttpClient, HttpErrorResponse } 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 { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' -import { PortalMessageService } from '@onecx/portal-integration-angular' +import { ConfigurationService, PortalMessageService } from '@onecx/portal-integration-angular' import { HttpLoaderFactory } from 'src/app/shared/shared.module' import { ThemeDetailComponent } from './theme-detail.component' +import { ThemesAPIService } from 'src/app/generated' +import { of, throwError } from 'rxjs' +import { ActivatedRoute, Router } from '@angular/router' +import { environment } from 'src/environments/environment' +import FileSaver from 'file-saver' describe('ThemeDetailComponent', () => { let component: ThemeDetailComponent let fixture: ComponentFixture - const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) + 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: 'de' + } + + const themesApiSpy = jasmine.createSpyObj('ThemesAPIService', [ + 'getThemeById', + 'deleteTheme', + 'exportThemes' + ]) beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -29,11 +51,28 @@ describe('ThemeDetailComponent', () => { } }) ], + providers: [ + { + provide: PortalMessageService, + useValue: msgServiceSpy + }, + { + provide: ConfigurationService, + useValue: configServiceSpy + }, + { + provide: ThemesAPIService, + useValue: themesApiSpy + } + ], schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(), - msgServiceSpy.success.calls.reset() + }).compileComponents() + msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() - msgServiceSpy.info.calls.reset() + themesApiSpy.getThemeById.and.returnValue(of({}) as any) + themesApiSpy.getThemeById.calls.reset() + themesApiSpy.exportThemes.and.returnValue(of({}) as any) + themesApiSpy.exportThemes.calls.reset() })) beforeEach(() => { @@ -45,4 +84,351 @@ describe('ThemeDetailComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) + + it('should create with provided id and get theme', async () => { + const id = 'themeId' + const route = TestBed.inject(ActivatedRoute) + spyOn(route.snapshot.paramMap, 'get').and.returnValue(id) + configServiceSpy.lang = 'de' + + // recreate component to test constructor + fixture = TestBed.createComponent(ThemeDetailComponent) + component = fixture.componentInstance + fixture.detectChanges() + + themesApiSpy.getThemeById.calls.reset() + component.loading = true + + await component.ngOnInit() + + expect(component.themeId).toBe(id) + expect(component.dateFormat).toBe('dd.MM.yyyy HH:mm:ss') + expect(themesApiSpy.getThemeById).toHaveBeenCalledOnceWith({ id: id }) + expect(component.loading).toBe(false) + }) + + it('should create with correct dateFormat', async () => { + configServiceSpy.lang = 'pl' + + // recreate component to test constructor + fixture = TestBed.createComponent(ThemeDetailComponent) + component = fixture.componentInstance + fixture.detectChanges() + + expect(component.dateFormat).toBe('medium') + }) + + it('should load theme and action translations on successful call', async () => { + const themeResponse = { + resource: { + name: 'themeName' + }, + workspaces: [ + { + workspaceName: 'workspace', + description: 'workspaceDesc' + } + ] + } + themesApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + + const translateService = TestBed.inject(TranslateService) + const actionsTranslations = { + 'ACTIONS.NAVIGATION.CLOSE': 'actionNavigationClose', + 'ACTIONS.NAVIGATION.CLOSE.TOOLTIP': 'actionNavigationCloseTooltip', + 'ACTIONS.EDIT.LABEL': 'actionEditLabel', + 'ACTIONS.EDIT.TOOLTIP': 'actionEditTooltip', + 'ACTIONS.EXPORT.LABEL': 'actionExportLabel', + 'ACTIONS.EXPORT.TOOLTIP': 'actionExportTooltip', + 'ACTIONS.DELETE.LABEL': 'actionDeleteLabel', + 'ACTIONS.DELETE.TOOLTIP': 'actionDeleteTooltip', + 'ACTIONS.DELETE.THEME_MESSAGE': '{{ITEM}} actionDeleteThemeMessage' + } + const generalTranslations = { + 'DETAIL.CREATION_DATE': 'detailCreationDate', + 'DETAIL.TOOLTIPS.CREATION_DATE': 'detailTooltipsCreationDate', + 'DETAIL.MODIFICATION_DATE': 'detailModificationDate', + 'DETAIL.TOOLTIPS.MODIFICATION_DATE': 'detailTooltipsModificationDate', + 'THEME.WORKSPACES': 'themeWorkspaces', + 'THEME.TOOLTIPS.WORKSPACES': 'themeTooltipsWorkspaces' + } + spyOn(translateService, 'get').and.returnValues(of(actionsTranslations), of(generalTranslations)) + + await component.ngOnInit() + + expect(component.theme).toEqual(themeResponse['resource']) + expect(component.usedInWorkspace).toEqual(themeResponse['workspaces']) + + expect(component.actions.length).toBe(4) + const closeAction = component.actions.filter( + (a) => a.label === 'actionNavigationClose' && a.title === 'actionNavigationCloseTooltip' + )[0] + spyOn(component, 'close') + closeAction.actionCallback() + expect(component.close).toHaveBeenCalledTimes(1) + + const editAction = component.actions.filter( + (a) => a.label === 'actionEditLabel' && a.title === 'actionEditTooltip' + )[0] + const router = TestBed.inject(Router) + spyOn(router, 'navigate') + editAction.actionCallback() + expect(router.navigate).toHaveBeenCalledOnceWith(['./edit'], jasmine.any(Object)) + + const exportAction = component.actions.filter( + (a) => a.label === 'actionExportLabel' && a.title === 'actionExportTooltip' + )[0] + spyOn(component, 'onExportTheme') + exportAction.actionCallback() + expect(component.onExportTheme).toHaveBeenCalledTimes(1) + + const deleteAction = component.actions.filter( + (a) => a.label === 'actionDeleteLabel' && a.title === 'actionDeleteTooltip' + )[0] + expect(component.themeDeleteVisible).toBe(false) + expect(component.themeDeleteMessage).toBe('') + deleteAction.actionCallback() + expect(component.themeDeleteVisible).toBe(true) + expect(component.themeDeleteMessage).toBe('themeName actionDeleteThemeMessage') + }) + + it('should load prepare object details on successfull call', async () => { + const themeResponse = { + resource: { + name: 'themeName', + creationDate: 'myCreDate', + modificationDate: 'myModDate' + }, + workspaces: [ + { + workspaceName: 'portal1' + }, + { + workspaceName: 'myPortal' + } + ] + } + themesApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + + const translateService = TestBed.inject(TranslateService) + const actionsTranslations = { + 'ACTIONS.NAVIGATION.CLOSE': 'actionNavigationClose', + 'ACTIONS.NAVIGATION.CLOSE.TOOLTIP': 'actionNavigationCloseTooltip', + 'ACTIONS.EDIT.LABEL': 'actionEditLabel', + 'ACTIONS.EDIT.TOOLTIP': 'actionEditTooltip', + 'ACTIONS.EXPORT.LABEL': 'actionExportLabel', + 'ACTIONS.EXPORT.TOOLTIP': 'actionExportTooltip', + 'ACTIONS.DELETE.LABEL': 'actionDeleteLabel', + 'ACTIONS.DELETE.TOOLTIP': 'actionDeleteTooltip', + 'ACTIONS.DELETE.THEME_MESSAGE': '{{ITEM}} actionDeleteThemeMessage' + } + const generalTranslations = { + 'DETAIL.CREATION_DATE': 'detailCreationDate', + 'DETAIL.TOOLTIPS.CREATION_DATE': 'detailTooltipsCreationDate', + 'DETAIL.MODIFICATION_DATE': 'detailModificationDate', + 'DETAIL.TOOLTIPS.MODIFICATION_DATE': 'detailTooltipsModificationDate', + 'THEME.WORKSPACES': 'themeWorkspaces', + 'THEME.TOOLTIPS.WORKSPACES': 'themeTooltipsWorkspaces' + } + spyOn(translateService, 'get').and.returnValues(of(actionsTranslations), of(generalTranslations)) + + await component.ngOnInit() + + expect(component.themePortalList).toBe('myPortal, portal1') + expect(component.objectDetails.length).toBe(3) + const creationDate = component.objectDetails.filter( + (detail) => detail.label === 'detailCreationDate' && detail.tooltip === 'detailTooltipsCreationDate' + )[0] + expect(creationDate.value).toBe('myCreDate') + + const modificationDate = component.objectDetails.filter( + (detail) => detail.label === 'detailModificationDate' && detail.tooltip === 'detailTooltipsModificationDate' + )[0] + expect(modificationDate.value).toBe('myModDate') + + const workspaces = component.objectDetails.filter( + (detail) => detail.label === 'themeWorkspaces' && detail.tooltip === 'themeTooltipsWorkspaces' + )[0] + expect(workspaces.value).toBe('myPortal, portal1') + }) + + it('should display not found error and close page on theme fetch failure', () => { + spyOn(component, 'close') + themesApiSpy.getThemeById.and.returnValue( + throwError( + () => + new HttpErrorResponse({ + error: 'err: was not found' + }) + ) + ) + + component.ngOnInit() + + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ + summaryKey: 'THEME.LOAD_ERROR', + detailKey: 'THEME.NOT_FOUND' + }) + expect(component.close).toHaveBeenCalledTimes(1) + }) + + it('should display catched error and close page on theme fetch failure', () => { + spyOn(component, 'close') + themesApiSpy.getThemeById.and.returnValue( + throwError( + () => + new HttpErrorResponse({ + error: 'does not contain checked string' + }) + ) + ) + + component.ngOnInit() + + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ + summaryKey: 'THEME.LOAD_ERROR', + detailKey: 'does not contain checked string' + }) + expect(component.close).toHaveBeenCalledTimes(1) + }) + + it('should return empty string if theme has no portals', () => { + component.theme = undefined + + const result = component.prepareUsedInPortalList() + expect(result).toBe('') + }) + + it('should navigate back on close', () => { + const router = TestBed.inject(Router) + spyOn(router, 'navigate') + + component.close() + + expect(router.navigate).toHaveBeenCalledOnceWith(['./..'], jasmine.any(Object)) + }) + + it('should set header image url with prefix when theme logo doesnt have http/https', async () => { + const themeResponse = { + resource: { + name: 'themeName', + logoUrl: 'logo123.png' + }, + workspaces: [] + } + themesApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + + await component.ngOnInit() + + expect(component.headerImageUrl).toBe(`${environment.apiPrefix}logo123.png`) + }) + + it('should set header image url without prefix when theme logo has http/https', async () => { + const themeResponse = { + resource: { + name: 'themeName', + logoUrl: 'http://external.com/logo123.png' + }, + workspaces: [] + } + themesApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) + + await component.ngOnInit() + + expect(component.headerImageUrl).toBe('http://external.com/logo123.png') + }) + + it('should hide dialog, inform and navigate on successfull deletion', () => { + const router = TestBed.inject(Router) + spyOn(router, 'navigate') + themesApiSpy.deleteTheme.and.returnValue(of({}) as any) + component.themeDeleteVisible = true + + component.confirmThemeDeletion() + + expect(component.themeDeleteVisible).toBe(false) + expect(router.navigate).toHaveBeenCalledOnceWith(['..'], jasmine.any(Object)) + expect(msgServiceSpy.success).toHaveBeenCalledOnceWith({ summaryKey: 'ACTIONS.DELETE.THEME_OK' }) + }) + + it('should hide dialog and display error on failed deletion', () => { + themesApiSpy.deleteTheme.and.returnValue( + throwError( + () => + new HttpErrorResponse({ + error: { + message: 'errorMessage' + } + }) + ) + ) + component.themeDeleteVisible = true + + component.confirmThemeDeletion() + + expect(component.themeDeleteVisible).toBe(false) + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ + summaryKey: 'ACTIONS.DELETE.THEME_NOK', + detailKey: 'errorMessage' + }) + }) + + it('should save file on theme export', () => { + spyOn(JSON, 'stringify').and.returnValue('themejson') + spyOn(FileSaver, 'saveAs') + + themesApiSpy.exportThemes.and.returnValue( + of({ + themes: { + themeName: { + version: 1, + logoUrl: 'url' + } + } + }) as any + ) + + component.theme = { + version: 1, + name: 'themeName', + logoUrl: 'url', + + creationDate: 'creationDate', + creationUser: 'createionUser', + modificationDate: 'modificationDate', + modificationUser: 'modificationUser', + id: 'id' + } + + component.onExportTheme() + + expect(themesApiSpy.exportThemes).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ exportThemeRequest: { names: ['themeName'] } }) + ) + expect(JSON.stringify).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + themes: { + themeName: { + version: 1, + logoUrl: 'url' + } + } + }), + null, + 2 + ) + expect(FileSaver.saveAs).toHaveBeenCalledOnceWith(jasmine.any(Blob), 'themeName_Theme.json') + }) + + it('should display error on theme export fail', () => { + themesApiSpy.exportThemes.and.returnValue(throwError(() => new Error())) + + component.theme = { + name: 'themeName' + } + + component.onExportTheme() + + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ summaryKey: 'ACTIONS.EXPORT.EXPORT_THEME_FAIL' }) + }) }) diff --git a/src/app/theme/theme-detail/theme-detail.component.ts b/src/app/theme/theme-detail/theme-detail.component.ts index dd8c8bd..5532a8d 100644 --- a/src/app/theme/theme-detail/theme-detail.component.ts +++ b/src/app/theme/theme-detail/theme-detail.component.ts @@ -165,7 +165,7 @@ export class ThemeDetailComponent implements OnInit { } } - private close(): void { + public close(): void { this.router.navigate(['./..'], { relativeTo: this.route }) } diff --git a/src/app/theme/theme-import/theme-import.component.spec.ts b/src/app/theme/theme-import/theme-import.component.spec.ts index 8c3b9d1..dfe0074 100644 --- a/src/app/theme/theme-import/theme-import.component.spec.ts +++ b/src/app/theme/theme-import/theme-import.component.spec.ts @@ -8,12 +8,17 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core' import { PortalMessageService } from '@onecx/portal-integration-angular' import { HttpLoaderFactory } from 'src/app/shared/shared.module' import { ThemeImportComponent } from './theme-import.component' +import { ThemesAPIService } from 'src/app/generated' +import { of, throwError } from 'rxjs' +import { Router } from '@angular/router' describe('ThemeImportComponent', () => { let component: ThemeImportComponent let fixture: ComponentFixture - const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) + const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error']) + + const themeApiSpy = jasmine.createSpyObj('ThemesAPIService', ['getThemes', 'importThemes']) beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -29,11 +34,18 @@ describe('ThemeImportComponent', () => { } }) ], + providers: [ + { provide: PortalMessageService, useValue: msgServiceSpy }, + { + provide: ThemesAPIService, + useValue: themeApiSpy + } + ], schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(), - msgServiceSpy.success.calls.reset() + }).compileComponents() + themeApiSpy.getThemes.and.returnValue(of({ stream: [] }) as any) + msgServiceSpy.success.calls.reset() msgServiceSpy.error.calls.reset() - msgServiceSpy.info.calls.reset() })) beforeEach(() => { @@ -44,5 +56,152 @@ describe('ThemeImportComponent', () => { it('should create', () => { expect(component).toBeTruthy() + expect(component.httpHeaders.get('Content-Type')).toBe('application/json') + expect(component.themes).toEqual([]) + }) + + it('should initialize themes and headers onInit', () => { + const themeArr = [ + { + name: 'theme1' + }, + { + name: 'theme2' + } + ] + const themesResponse = { + stream: themeArr + } + themeApiSpy.getThemes.and.returnValue(of(themesResponse as any)) + component.ngOnInit() + + expect(component.themes).toEqual(themeArr) + }) + + it('should read file on theme import select', async () => { + const themeSnapshot = JSON.stringify({ + themes: { + themeName: { + logoUrl: 'logo_url', + properties: { + general: { + 'primary-color': '#000000' + } + } + } + } + }) + const file = new File([themeSnapshot], 'file_name') + const event = jasmine.createSpyObj('event', [], { files: [file] }) + + await component.onImportThemeSelect(event) + + expect(component.themeImportError).toBe(false) + expect(component.themeSnapshot).toBeDefined() + expect(component.properties).toEqual({ + general: { + 'primary-color': '#000000' + } + }) + expect(component.themeNameExists).toBe(false) + }) + + it('should indicate and log error on invalid data', async () => { + spyOn(console, 'error') + const file = new File(['{"invalid": "invalidProperty"}'], 'file_name') + const event = jasmine.createSpyObj('event', [], { files: [file] }) + + await component.onImportThemeSelect(event) + + expect(component.themeImportError).toBe(true) + expect(component.themeSnapshot).toBe(null) + expect(console.error).toHaveBeenCalledOnceWith('Theme Import Error: not valid data ') + // TODO: if error is visible + }) + + it('should log error on data parsing error', async () => { + spyOn(console, 'error') + + const file = new File(['notJsonFile'], 'file_name') + const event = jasmine.createSpyObj('event', [], { files: [file] }) + + await component.onImportThemeSelect(event) + + expect(console.error).toHaveBeenCalledOnceWith('Theme Import Parse Error', jasmine.any(Object)) + expect(component.themeSnapshot).toBe(null) + }) + + it('should indicate theme name existance if already present', async () => { + component.themes = [ + { + name: 'themeName' + } + ] + const themeSnapshot = JSON.stringify({ + themes: { + themeName: { + logoUrl: 'logo_url' + } + } + }) + const file = new File([themeSnapshot], 'file_name') + const event = jasmine.createSpyObj('event', [], { files: [file] }) + + await component.onImportThemeSelect(event) + + expect(component.themeImportError).toBe(false) + expect(component.themeSnapshot).toBeDefined() + expect(component.themeNameExists).toBe(true) + // TODO: if error is visible + }) + + it('should emit displayThemeImportChange on import hide', () => { + spyOn(component.displayThemeImportChange, 'emit') + + component.onImportThemeHide() + + expect(component.displayThemeImportChange.emit).toHaveBeenCalledOnceWith(false) + }) + + it('should clear error and import data on import clear', () => { + component.themeSnapshot = { + themes: { + themeName: { + logoUrl: 'logo_url' + } + } + } + component.themeImportError = true + + component.onImportThemeClear() + + expect(component.themeSnapshot).toBeNull() + expect(component.themeImportError).toBeFalse() + }) + + it('should inform and navigate to new theme on import success', () => { + const router = TestBed.inject(Router) + spyOn(router, 'navigate') + themeApiSpy.importThemes.and.returnValue( + of({ + id: 'themeId', + name: 'name' + } as any) + ) + spyOn(component.uploadEmitter, 'emit') + + component.onThemeUpload() + + expect(msgServiceSpy.success).toHaveBeenCalledOnceWith({ summaryKey: 'THEME.IMPORT.IMPORT_THEME_SUCCESS' }) + expect(component.uploadEmitter.emit).toHaveBeenCalledTimes(1) + expect(router.navigate).toHaveBeenCalledOnceWith(['./themeId'], jasmine.any(Object)) + }) + + it('should display error on api call fail during upload', () => { + themeApiSpy.importThemes.and.returnValue(throwError(() => new Error())) + + component.onThemeUpload() + + expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ summaryKey: 'THEME.IMPORT.IMPORT_THEME_FAIL' }) }) }) diff --git a/src/app/theme/theme-import/theme-import.component.ts b/src/app/theme/theme-import/theme-import.component.ts index db28bd7..d8b260d 100644 --- a/src/app/theme/theme-import/theme-import.component.ts +++ b/src/app/theme/theme-import/theme-import.component.ts @@ -17,7 +17,7 @@ export class ThemeImportComponent implements OnInit { @Output() public displayThemeImportChange = new EventEmitter() @Output() public uploadEmitter = new EventEmitter() - private themes!: Theme[] + public themes!: Theme[] public themeName = '' public themeNameExists = false public themeImportError = false @@ -35,12 +35,12 @@ export class ThemeImportComponent implements OnInit { ngOnInit(): void { this.httpHeaders = new HttpHeaders() - this.httpHeaders.set('Content-Type', 'application/json') + this.httpHeaders = this.httpHeaders.set('Content-Type', 'application/json') this.getThemes(false) } - public onImportThemeSelect(event: { files: FileList }): void { - event.files[0].text().then((text) => { + public async onImportThemeSelect(event: { files: FileList }): Promise { + return event.files[0].text().then((text) => { this.themeSnapshot = null try { const themeSnapshot = JSON.parse(text) @@ -48,7 +48,7 @@ export class ThemeImportComponent implements OnInit { this.themeSnapshot = themeSnapshot this.themeImportError = false if (themeSnapshot.themes !== undefined) { - this.properties = themeSnapshot?.themes[0].properties + this.properties = themeSnapshot.themes[Object.keys(themeSnapshot.themes)[0]].properties } this.checkThemeExistence() } else { @@ -63,8 +63,8 @@ export class ThemeImportComponent implements OnInit { public checkThemeExistence() { this.themeNameExists = false - for (const { name } of this.themes) { - if (name === this.themeSnapshot?.themes) { + if (this.themeSnapshot?.themes) { + if (this.themes.find((theme) => Object.keys(this.themeSnapshot!.themes!).indexOf(theme.name!) > -1)) { this.themeNameExists = true } } @@ -98,7 +98,7 @@ export class ThemeImportComponent implements OnInit { private isThemeImportRequestDTO(obj: unknown): obj is ThemeSnapshot { const dto = obj as ThemeSnapshot - return !!(typeof dto === 'object' && dto) + return !!(typeof dto === 'object' && dto && dto.themes) } private getThemes(emit: boolean): void { diff --git a/src/app/theme/theme-search/theme-search.component.spec.ts b/src/app/theme/theme-search/theme-search.component.spec.ts index 9279c9e..3851716 100644 --- a/src/app/theme/theme-search/theme-search.component.spec.ts +++ b/src/app/theme/theme-search/theme-search.component.spec.ts @@ -3,17 +3,20 @@ 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 { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' -import { PortalMessageService } from '@onecx/portal-integration-angular' import { HttpLoaderFactory } from 'src/app/shared/shared.module' import { ThemeSearchComponent } from './theme-search.component' +import { ThemesAPIService } from 'src/app/generated' +import { of } from 'rxjs' +import { Router } from '@angular/router' +import { DataViewModule } from 'primeng/dataview' describe('ThemeSearchComponent', () => { let component: ThemeSearchComponent let fixture: ComponentFixture - const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) + const themeApiSpy = jasmine.createSpyObj('ThemesAPIService', ['getThemes']) beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -21,6 +24,7 @@ describe('ThemeSearchComponent', () => { imports: [ RouterTestingModule, HttpClientTestingModule, + DataViewModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -29,11 +33,14 @@ describe('ThemeSearchComponent', () => { } }) ], + providers: [ + { + provide: ThemesAPIService, + useValue: themeApiSpy + } + ], schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(), - msgServiceSpy.success.calls.reset() - msgServiceSpy.error.calls.reset() - msgServiceSpy.info.calls.reset() + }).compileComponents() })) beforeEach(() => { @@ -45,4 +52,132 @@ describe('ThemeSearchComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) + + it('should load themes and translations on initialization', async () => { + const translateService = TestBed.inject(TranslateService) + const actionsTranslations = { + 'ACTIONS.CREATE.THEME': 'actionsCreateTheme', + 'ACTIONS.CREATE.THEME.TOOLTIP': 'actionsCreateThemeTooltip', + 'ACTIONS.IMPORT.LABEL': 'actionsImportLabel', + 'ACTIONS.IMPORT.TOOLTIP': 'actionsImportTooltip' + } + const generalTranslations = { + 'THEME.NAME': 'themeName', + 'THEME.DESCRIPTION': 'themeDescription', + 'SEARCH.SORT_BY': 'searchSort', + 'SEARCH.FILTER': 'searchFilter', + 'SEARCH.FILTER_OF': 'searchFilterOf', + 'SEARCH.SORT_DIRECTION_ASC': 'searchSortDirectionsAsc', + 'SEARCH.SORT_DIRECTION_DESC': 'searchSortDirectionDesc', + 'GENERAL.TOOLTIP.VIEW_MODE_GRID': 'generalTooltipViewModeGrid', + 'GENERAL.TOOLTIP.VIEW_MODE_LIST': 'generalTooltipViewModeList', + 'GENERAL.TOOLTIP.VIEW_MODE_TABLE': 'generalTooltipViewModeTable' + } + spyOn(translateService, 'get').and.returnValues(of(actionsTranslations), of(generalTranslations)) + const themesResponse = { + stream: [ + { + name: 'theme1' + }, + { + name: 'theme2' + } + ] + } + const themesObservable = of(themesResponse as any) + themeApiSpy.getThemes.and.returnValue(themesObservable) + + await component.ngOnInit() + + expect(component.themes$).toEqual(themesObservable) + expect(component.actions.length).toBe(2) + const createAction = component.actions.filter( + (a) => a.label === 'actionsCreateTheme' && a.title === 'actionsCreateThemeTooltip' + )[0] + spyOn(component, 'onNewTheme') + createAction.actionCallback() + expect(component.onNewTheme).toHaveBeenCalledTimes(1) + + const importAction = component.actions.filter( + (a) => a.label === 'actionsImportLabel' && a.title === 'actionsImportTooltip' + )[0] + spyOn(component, 'onImportThemeClick') + importAction.actionCallback() + expect(component.onImportThemeClick).toHaveBeenCalledTimes(1) + + expect(component.dataViewControlsTranslations).toEqual({ + sortDropdownPlaceholder: generalTranslations['SEARCH.SORT_BY'], + filterInputPlaceholder: generalTranslations['SEARCH.FILTER'], + filterInputTooltip: + generalTranslations['SEARCH.FILTER_OF'] + + generalTranslations['THEME.NAME'] + + ', ' + + generalTranslations['THEME.DESCRIPTION'], + viewModeToggleTooltips: { + grid: generalTranslations['GENERAL.TOOLTIP.VIEW_MODE_GRID'], + list: generalTranslations['GENERAL.TOOLTIP.VIEW_MODE_LIST'] + }, + sortOrderTooltips: { + ascending: generalTranslations['SEARCH.SORT_DIRECTION_ASC'], + descending: generalTranslations['SEARCH.SORT_DIRECTION_DESC'] + }, + sortDropdownTooltip: generalTranslations['SEARCH.SORT_BY'] + }) + }) + + it('should navigate to theme detail on new theme callback', () => { + const router = TestBed.inject(Router) + spyOn(router, 'navigate') + + component.onNewTheme() + + expect(router.navigate).toHaveBeenCalledOnceWith(['./new'], jasmine.any(Object)) + }) + + it('should change viewMode on layout change', () => { + expect(component.viewMode).toBe('grid') + + component.onLayoutChange('list') + + expect(component.viewMode).toBe('list') + }) + + it('should filter dataView on filter change', () => { + component.dv = jasmine.createSpyObj('DataView', ['filter']) + component.filter = '' + const myFilter = 'myTheme' + + component.onFilterChange(myFilter) + + expect(component.filter).toBe(myFilter) + expect(component.dv!.filter).toHaveBeenCalledOnceWith(myFilter, 'contains') + }) + + it('should change field to sort by on sort change', () => { + component.sortField = 'name' + + component.onSortChange('description') + + expect(component.sortField).toBe('description') + }) + + it('should change sorting direction on sorting direction change', () => { + component.sortOrder = 1 + + component.onSortDirChange(true) + + expect(component.sortOrder).toBe(-1) + + component.onSortDirChange(false) + + expect(component.sortOrder).toBe(1) + }) + + it('should show import dialog on import theme click', () => { + component.themeImportDialogVisible = false + + component.onImportThemeClick() + + expect(component.themeImportDialogVisible).toBe(true) + }) }) diff --git a/src/app/theme/theme-search/theme-search.component.ts b/src/app/theme/theme-search/theme-search.component.ts index 9d5c083..eec917c 100644 --- a/src/app/theme/theme-search/theme-search.component.ts +++ b/src/app/theme/theme-search/theme-search.component.ts @@ -99,7 +99,7 @@ export class ThemeSearchComponent implements OnInit { } } - private onNewTheme(): void { + public onNewTheme(): void { this.router.navigate(['./new'], { relativeTo: this.route }) } public onLayoutChange(viewMode: string): void { diff --git a/src/app/theme/theme-variables.ts b/src/app/theme/theme-variables.ts index dc37ba7..9da1e08 100644 --- a/src/app/theme/theme-variables.ts +++ b/src/app/theme/theme-variables.ts @@ -10,10 +10,8 @@ export const themeVariables: ThemeVariablesType = { topbar: [ 'topbar-bg-color', 'topbar-item-text-color', - 'topbar-bg-color', 'topbar-text-color', 'topbar-left-bg-color', - 'topbar-item-text-color', 'topbar-item-text-hover-bg-color', 'topbar-menu-button-bg-color', 'logo-color' diff --git a/src/app/theme/theme.module.ts b/src/app/theme/theme.module.ts index 4572ea3..b83399a 100644 --- a/src/app/theme/theme.module.ts +++ b/src/app/theme/theme.module.ts @@ -4,6 +4,7 @@ import { FormsModule } from '@angular/forms' import { RouterModule, Routes } from '@angular/router' import { MissingTranslationHandler, TranslateLoader, TranslateModule } from '@ngx-translate/core' import { FieldsetModule } from 'primeng/fieldset' +import { ConfirmDialogModule } from 'primeng/confirmdialog' import { MFE_INFO, PortalCoreModule, MyMissingTranslationHandler } from '@onecx/portal-integration-angular' @@ -72,6 +73,7 @@ const routes: Routes = [ imports: [ FormsModule, FieldsetModule, + ConfirmDialogModule, PortalCoreModule.forMicroFrontend(), [RouterModule.forChild(routes)], SharedModule,