From 4685dcd450b93b662dffdbdf62d5f8332d05dc12 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Stephan=20M=C3=BCller?= Date: Tue, 30 Jul 2019 17:55:04 +0200 Subject: [PATCH] mgr/dashboard: Add tests for confirmation modal MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Fixes: https://tracker.ceph.com/issues/40828 Signed-off-by: Stephan Müller --- .../confirmation-modal.component.spec.ts | 179 +++++++++++++++++- .../confirmation-modal.component.ts | 24 ++- ...tical-confirmation-modal.component.spec.ts | 16 +- .../frontend/src/testing/unit-test-helper.ts | 29 ++- 4 files changed, 220 insertions(+), 28 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts index 2c1f76a1d36..21a82ca4f3c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts @@ -1,37 +1,198 @@ +import { Component, NgModule, NO_ERRORS_SCHEMA, TemplateRef, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; -import { BsModalRef } from 'ngx-bootstrap/modal'; +import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal'; -import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { By } from '@angular/platform-browser'; +import { + configureTestBed, + FixtureHelper, + i18nProviders, + modalServiceShow +} from '../../../../testing/unit-test-helper'; import { BackButtonComponent } from '../back-button/back-button.component'; import { ModalComponent } from '../modal/modal.component'; import { SubmitButtonComponent } from '../submit-button/submit-button.component'; import { ConfirmationModalComponent } from './confirmation-modal.component'; +@NgModule({ + entryComponents: [ConfirmationModalComponent] +}) +export class MockModule {} + +@Component({ + template: ` + + The description of the confirmation modal is mandatory. + + ` +}) +class MockComponent { + @ViewChild('fillTpl', { static: true }) + fillTpl: TemplateRef; + modalRef: BsModalRef; + returnValue: any; + + // Normally private, but public is needed by tests + constructor(public modalService: BsModalService) {} + + private openModal(extendBaseState: object = {}) { + this.modalRef = this.modalService.show(ConfirmationModalComponent, { + initialState: Object.assign( + { + titleText: 'Title is a must have', + buttonText: 'Action label', + bodyTpl: this.fillTpl, + onSubmit: () => { + this.returnValue = 'The submit action has to hide manually.'; + this.modalRef.hide(); + } + }, + extendBaseState + ) + }); + } + + basicModal() { + this.openModal(); + } +} + describe('ConfirmationModalComponent', () => { let component: ConfirmationModalComponent; let fixture: ComponentFixture; + let mockComponent: MockComponent; + let mockFixture: ComponentFixture; + let modalService: BsModalService; + let fh: FixtureHelper; + + /** + * The hide method of `BsModalService` doesn't emit events during tests that's why it's mocked. + * + * The only events of hide are `null`, `'backdrop-click'` and `'esc'` as described here: + * https://ngx-universal.herokuapp.com/#/modals#service-events + */ + const hide = (x: string) => modalService.onHide.emit(null || x); + + const expectReturnValue = (v: string) => expect(mockComponent.returnValue).toBe(v); configureTestBed({ declarations: [ ConfirmationModalComponent, BackButtonComponent, - SubmitButtonComponent, - ModalComponent + MockComponent, + ModalComponent, + SubmitButtonComponent ], - imports: [ReactiveFormsModule, RouterTestingModule], - providers: [BsModalRef, i18nProviders] + schemas: [NO_ERRORS_SCHEMA], + imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule, RouterTestingModule], + providers: [BsModalRef, i18nProviders, SubmitButtonComponent] }); beforeEach(() => { - fixture = TestBed.createComponent(ConfirmationModalComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + fh = new FixtureHelper(); + mockFixture = TestBed.createComponent(MockComponent); + mockComponent = mockFixture.componentInstance; + mockFixture.detectChanges(); + modalService = TestBed.get(BsModalService); + spyOn(modalService, 'show').and.callFake((_modalComp, config) => { + const data = modalServiceShow(ConfirmationModalComponent, config); + fixture = data.fixture; + component = data.component; + spyOn(component.modalRef, 'hide').and.callFake(hide); + fh.updateFixture(fixture); + return data.ref; + }); }); it('should create', () => { + mockComponent.basicModal(); expect(component).toBeTruthy(); }); + + describe('Throws errors', () => { + const expectError = (config: object, expected: string) => { + mockComponent.basicModal(); + component = Object.assign(component, config); + expect(() => component.ngOnInit()).toThrowError(expected); + }; + + it('has no submit action defined', () => { + expectError( + { + onSubmit: undefined + }, + 'No submit action defined' + ); + }); + + it('has no title defined', () => { + expectError( + { + titleText: undefined + }, + 'No title defined' + ); + }); + + it('has no action name defined', () => { + expectError( + { + buttonText: undefined + }, + 'No action name defined' + ); + }); + + it('has no description defined', () => { + expectError( + { + bodyTpl: undefined + }, + 'No description defined' + ); + }); + }); + + describe('basics', () => { + beforeEach(() => { + mockComponent.basicModal(); + spyOn(mockComponent.modalRef, 'hide').and.callFake(hide); + }); + + it('should show the correct title', () => { + expect(fh.getText('.modal-title')).toBe('Title is a must have'); + }); + + it('should show the correct action name', () => { + expect(fh.getText('.tc_submitButton')).toBe('Action label'); + }); + + it('should use the correct submit action', () => { + // In order to ignore the `ElementRef` usage of `SubmitButtonComponent` + spyOn( + fixture.debugElement.query(By.directive(SubmitButtonComponent)).componentInstance, + 'focusButton' + ); + fh.clickElement('.tc_submitButton'); + expect(mockComponent.modalRef.hide).toHaveBeenCalledTimes(1); + expect(component.modalRef.hide).toHaveBeenCalledTimes(0); + expectReturnValue('The submit action has to hide manually.'); + }); + + it('should use the default cancel action', () => { + fh.clickElement('.tc_backButton'); + expect(mockComponent.modalRef.hide).toHaveBeenCalledTimes(0); + expect(component.modalRef.hide).toHaveBeenCalledTimes(1); + expectReturnValue(undefined); + }); + + it('should show the description', () => { + expect(fh.getText('.modal-body')).toBe( + 'The description of the confirmation modal is mandatory.' + ); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts index 9765fed85e4..617a7068603 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts @@ -8,18 +8,21 @@ import { BsModalRef } from 'ngx-bootstrap/modal'; templateUrl: './confirmation-modal.component.html', styleUrls: ['./confirmation-modal.component.scss'] }) -export class ConfirmationModalComponent implements OnInit { - bodyData: object; +export class ConfirmationModalComponent implements OnInit, OnDestroy { + // Needed bodyTpl: TemplateRef; buttonText: string; - onSubmit: Function; - onCancel: Function; titleText: string; + onSubmit: Function; - bodyContext: object; - confirmationForm: FormGroup; + // Optional + bodyData?: object; + onCancel?: Function; + bodyContext?: object; + // Component only boundCancel = this.cancel.bind(this); + confirmationForm: FormGroup; constructor(public modalRef: BsModalRef) { this.confirmationForm = new FormGroup({}); @@ -28,6 +31,15 @@ export class ConfirmationModalComponent implements OnInit { ngOnInit() { this.bodyContext = this.bodyContext || {}; this.bodyContext['$implicit'] = this.bodyData; + if (!this.onSubmit) { + throw new Error('No submit action defined'); + } else if (!this.buttonText) { + throw new Error('No action name defined'); + } else if (!this.titleText) { + throw new Error('No title defined'); + } else if (!this.bodyTpl) { + throw new Error('No description defined'); + } } cancel() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts index a81d539d54a..33416d09925 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts @@ -6,7 +6,7 @@ import { By } from '@angular/platform-browser'; import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal'; import { Observable, Subscriber, timer as observableTimer } from 'rxjs'; -import { configureTestBed } from '../../../../testing/unit-test-helper'; +import { configureTestBed, modalServiceShow } from '../../../../testing/unit-test-helper'; import { DirectivesModule } from '../../directives/directives.module'; import { CriticalConfirmationModalComponent } from './critical-confirmation-modal.component'; @@ -103,17 +103,11 @@ describe('CriticalConfirmationModalComponent', () => { beforeEach(() => { mockFixture = TestBed.createComponent(MockComponent); mockComponent = mockFixture.componentInstance; - // Mocking the modals as a lot would be left over spyOn(mockComponent.modalService, 'show').and.callFake((_modalComp, config) => { - const ref = new BsModalRef(); - fixture = TestBed.createComponent(CriticalConfirmationModalComponent); - component = fixture.componentInstance; - if (config.initialState) { - component = Object.assign(component, config.initialState); - } - fixture.detectChanges(); - ref.content = component; - return ref; + const data = modalServiceShow(CriticalConfirmationModalComponent, config); + fixture = data.fixture; + component = data.component; + return data.ref; }); mockComponent.openCtrlDriven(); mockFixture.detectChanges(); diff --git a/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts b/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts index b9148f1f017..5c795dcaab5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts +++ b/src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts @@ -1,9 +1,10 @@ -import { LOCALE_ID, TRANSLATIONS, TRANSLATIONS_FORMAT } from '@angular/core'; +import { LOCALE_ID, TRANSLATIONS, TRANSLATIONS_FORMAT, Type } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { AbstractControl } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { I18n } from '@ngx-translate/i18n-polyfill'; +import { BsModalRef } from 'ngx-bootstrap/modal'; import { TableActionsComponent } from '../app/shared/datatable/table-actions/table-actions.component'; import { Icons } from '../app/shared/enum/icons.enum'; @@ -184,10 +185,34 @@ export class FormHelper { } } +/** + * Use this to mock 'ModalService.show' to make the embedded component with it's fixture usable + * in tests. The function gives back all needed parts including the modal reference. + * + * Please make sure to call this function *inside* your mock and return the reference at the end. + */ +export function modalServiceShow(componentClass: Type, modalConfig) { + const ref = new BsModalRef(); + const fixture = TestBed.createComponent(componentClass); + let component = fixture.componentInstance; + if (modalConfig.initialState) { + component = Object.assign(component, modalConfig.initialState); + } + fixture.detectChanges(); + ref.content = component; + return { ref, fixture, component }; +} + export class FixtureHelper { fixture: ComponentFixture; - constructor(fixture: ComponentFixture) { + constructor(fixture?: ComponentFixture) { + if (fixture) { + this.updateFixture(fixture); + } + } + + updateFixture(fixture: ComponentFixture) { this.fixture = fixture; } -- 2.39.5