]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Add tests for confirmation modal
authorStephan Müller <smueller@suse.com>
Tue, 30 Jul 2019 15:55:04 +0000 (17:55 +0200)
committerStephan Müller <smueller@suse.com>
Wed, 4 Sep 2019 14:22:33 +0000 (16:22 +0200)
Fixes: https://tracker.ceph.com/issues/40828
Signed-off-by: Stephan Müller <smueller@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/confirmation-modal/confirmation-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts

index 2c1f76a1d365d2646f01d727d959125c6184a0a1..21a82ca4f3c3889233c3b6bc7bc3d674b727f66e 100644 (file)
+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: `
+    <ng-template #fillTpl>
+      The description of the confirmation modal is mandatory.
+    </ng-template>
+  `
+})
+class MockComponent {
+  @ViewChild('fillTpl', { static: true })
+  fillTpl: TemplateRef<any>;
+  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<ConfirmationModalComponent>;
+  let mockComponent: MockComponent;
+  let mockFixture: ComponentFixture<MockComponent>;
+  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.'
+      );
+    });
+  });
 });
index 9765fed85e483b7266aa52080ee5112a00418826..617a7068603ec556a9bca66d48e59782a93b9daf 100644 (file)
@@ -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<any>;
   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() {
index a81d539d54a926b1051be3bcb4f5955bd33130a8..33416d0992591f9c88269b68ef018f1a709e8829 100644 (file)
@@ -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();
index b9148f1f017244f3a2ae3c3b1f038e7a683e9222..5c795dcaab554c9c3b8f637fb4e890e1fdaeebdd 100644 (file)
@@ -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<any>, 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<any>;
 
-  constructor(fixture: ComponentFixture<any>) {
+  constructor(fixture?: ComponentFixture<any>) {
+    if (fixture) {
+      this.updateFixture(fixture);
+    }
+  }
+
+  updateFixture(fixture: ComponentFixture<any>) {
     this.fixture = fixture;
   }