]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Added unit tests
authorAfreen Misbah <afreen@ibm.com>
Thu, 12 Feb 2026 10:36:53 +0000 (16:06 +0530)
committerAfreen Misbah <afreen@ibm.com>
Mon, 16 Feb 2026 10:23:28 +0000 (15:53 +0530)
Assisted-by: ChatGPT
Signed-off-by: Afreen Misbah <afreen@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.spec.ts

index 2774459b713064be11b157e65648a199a5bb83f1..831eb7458b1fdc26038632af86f00bc754fec979 100644 (file)
@@ -1,14 +1,38 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { of, Subject, throwError } from 'rxjs';
 
 import { OverviewComponent } from './overview.component';
+import { HealthService } from '~/app/shared/api/health.service';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+import { HealthSnapshotMap } from '~/app/shared/models/health.interface';
 
-describe('OverviewComponent', () => {
+describe('OverviewComponent (Jest)', () => {
   let component: OverviewComponent;
   let fixture: ComponentFixture<OverviewComponent>;
 
+  let mockHealthService: {
+    getHealthSnapshot: jest.Mock;
+  };
+
+  let mockRefreshIntervalService: {
+    intervalData$: Subject<void>;
+  };
+
   beforeEach(async () => {
+    mockHealthService = {
+      getHealthSnapshot: jest.fn()
+    };
+
+    mockRefreshIntervalService = {
+      intervalData$: new Subject<void>()
+    };
+
     await TestBed.configureTestingModule({
-      imports: [OverviewComponent]
+      imports: [OverviewComponent],
+      providers: [
+        { provide: HealthService, useValue: mockHealthService },
+        { provide: RefreshIntervalService, useValue: mockRefreshIntervalService }
+      ]
     }).compileComponents();
 
     fixture = TestBed.createComponent(OverviewComponent);
@@ -16,7 +40,126 @@ describe('OverviewComponent', () => {
     fixture.detectChanges();
   });
 
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  // --------------------------------------------------
+  // CREATION
+  // --------------------------------------------------
+
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  // --------------------------------------------------
+  // refreshIntervalObs - success case
+  // --------------------------------------------------
+
+  it('should call healthService when interval emits', (done) => {
+    const mockResponse: HealthSnapshotMap = { status: 'OK' } as any;
+
+    mockHealthService.getHealthSnapshot.mockReturnValue(of(mockResponse));
+
+    component.healthData$.subscribe((data) => {
+      expect(data).toEqual(mockResponse);
+      expect(mockHealthService.getHealthSnapshot).toHaveBeenCalled();
+      done();
+    });
+
+    mockRefreshIntervalService.intervalData$.next();
+  });
+
+  // --------------------------------------------------
+  // refreshIntervalObs - error case (catchError → EMPTY)
+  // --------------------------------------------------
+
+  it('should return EMPTY when healthService throws error', (done) => {
+    mockHealthService.getHealthSnapshot.mockReturnValue(throwError(() => new Error('API Error')));
+
+    let emitted = false;
+
+    component.healthData$.subscribe({
+      next: () => {
+        emitted = true;
+      },
+      complete: () => {
+        expect(emitted).toBe(false);
+        done();
+      }
+    });
+
+    mockRefreshIntervalService.intervalData$.next();
+    mockRefreshIntervalService.intervalData$.complete();
+  });
+
+  // --------------------------------------------------
+  // refreshIntervalObs - exhaustMap behavior
+  // --------------------------------------------------
+
+  it('should ignore new interval emissions until previous completes', () => {
+    const interval$ = new Subject<void>();
+    const inner$ = new Subject<any>();
+
+    const mockRefreshService = {
+      intervalData$: interval$
+    };
+
+    const testComponent = new OverviewComponent(
+      mockHealthService as any,
+      mockRefreshService as any
+    );
+
+    mockHealthService.getHealthSnapshot.mockReturnValue(inner$);
+
+    testComponent.healthData$.subscribe();
+
+    // First emission
+    interval$.next();
+
+    // Second emission (should be ignored)
+    interval$.next();
+
+    expect(mockHealthService.getHealthSnapshot).toHaveBeenCalledTimes(1);
+
+    // Complete first inner observable
+    inner$.complete();
+
+    // Now it should allow another call
+    interval$.next();
+
+    expect(mockHealthService.getHealthSnapshot).toHaveBeenCalledTimes(2);
+  });
+
+  // --------------------------------------------------
+  // ngOnDestroy
+  // --------------------------------------------------
+
+  it('should complete destroy$ on destroy', () => {
+    const nextSpy = jest.spyOn((component as any).destroy$, 'next');
+    const completeSpy = jest.spyOn((component as any).destroy$, 'complete');
+
+    component.ngOnDestroy();
+
+    expect(nextSpy).toHaveBeenCalled();
+    expect(completeSpy).toHaveBeenCalled();
+  });
+
+  // --------------------------------------------------
+  // refreshIntervalObs manual test
+  // --------------------------------------------------
+
+  it('refreshIntervalObs should pipe intervalData$', (done) => {
+    const testFn = jest.fn().mockReturnValue(of('TEST'));
+
+    const obs$ = component.refreshIntervalObs(testFn);
+
+    obs$.subscribe((value) => {
+      expect(value).toBe('TEST');
+      expect(testFn).toHaveBeenCalled();
+      done();
+    });
+
+    mockRefreshIntervalService.intervalData$.next();
+  });
 });
index ebfa6cccca597634ddb8ceadd963162a683cbe68..5a8e63011f6258fed66146ab6d823dc3150057ee 100644 (file)
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { of } from 'rxjs';
 
 import { OverviewStorageCardComponent } from './overview-storage-card.component';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { FormatterService } from '~/app/shared/services/formatter.service';
 
-describe('OverviewStorageCardComponent', () => {
+describe('OverviewStorageCardComponent (Jest)', () => {
   let component: OverviewStorageCardComponent;
   let fixture: ComponentFixture<OverviewStorageCardComponent>;
 
+  let mockPrometheusService: {
+    getPrometheusQueryData: jest.Mock;
+  };
+
+  let mockFormatterService: {
+    formatToBinary: jest.Mock;
+    convertToUnit: jest.Mock;
+  };
+
+  const mockPrometheusResponse = {
+    result: [
+      {
+        metric: { application: 'Block' },
+        value: [0, 1024]
+      },
+      {
+        metric: { application: 'Filesystem' },
+        value: [0, 2048]
+      },
+      {
+        metric: { application: 'Object' },
+        value: [0, 0] // should be filtered
+      }
+    ]
+  };
+
   beforeEach(async () => {
+    mockPrometheusService = {
+      getPrometheusQueryData: jest.fn().mockReturnValue(of(mockPrometheusResponse))
+    };
+
+    mockFormatterService = {
+      formatToBinary: jest.fn().mockReturnValue([10, 'GiB']),
+      convertToUnit: jest.fn((value: number) => Number(value))
+    };
+
     await TestBed.configureTestingModule({
-      imports: [OverviewStorageCardComponent]
+      imports: [OverviewStorageCardComponent],
+      providers: [
+        { provide: PrometheusService, useValue: mockPrometheusService },
+        { provide: FormatterService, useValue: mockFormatterService }
+      ]
     }).compileComponents();
 
     fixture = TestBed.createComponent(OverviewStorageCardComponent);
     component = fixture.componentInstance;
-    fixture.detectChanges();
+    fixture.detectChanges(); // triggers ngOnInit
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
   });
 
+  // --------------------------------------------------
+  // CREATION
+  // --------------------------------------------------
+
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  // --------------------------------------------------
+  // TOTAL setter (truthy)
+  // --------------------------------------------------
+
+  it('should set total when valid value provided', () => {
+    component.total = 1024;
+
+    expect(component.totalRaw).toBe(10);
+    expect(component.totalRawUnit).toBe('GiB');
+  });
+
+  // --------------------------------------------------
+  // TOTAL setter (falsy)
+  // --------------------------------------------------
+
+  it('should not set total when formatter returns NaN', () => {
+    mockFormatterService.formatToBinary.mockReturnValue([NaN, 'GiB']);
+
+    component.total = 0;
+
+    expect(component.totalRaw).toBeUndefined();
+  });
+
+  // --------------------------------------------------
+  // USED setter
+  // --------------------------------------------------
+
+  it('should set used correctly', () => {
+    component.used = 2048;
+
+    expect(component.usedRaw).toBe(10);
+    expect(component.usedRawUnit).toBe('GiB');
+  });
+
+  // --------------------------------------------------
+  // TOGGLE
+  // --------------------------------------------------
+
+  it('should switch to RAW when toggled true', () => {
+    component.toggleRawCapacity(true);
+
+    expect(component.isRawCapacity).toBe(true);
+    expect(component.selectedCapacityType).toBe('raw');
+  });
+
+  it('should switch to USED when toggled false', () => {
+    component.toggleRawCapacity(false);
+
+    expect(component.isRawCapacity).toBe(false);
+    expect(component.selectedCapacityType).toBe('used');
+  });
+
+  it('should call Prometheus again when toggled', () => {
+    component.toggleRawCapacity(false);
+
+    expect(mockPrometheusService.getPrometheusQueryData).toHaveBeenCalledTimes(2);
+  });
+
+  // --------------------------------------------------
+  // ngOnInit data load
+  // --------------------------------------------------
+
+  it('should load and filter data on init', () => {
+    expect(mockPrometheusService.getPrometheusQueryData).toHaveBeenCalled();
+    expect(component.allData.length).toBe(2); // Object filtered (0 value)
+  });
+
+  // --------------------------------------------------
+  // FILTERING
+  // --------------------------------------------------
+
+  it('should filter displayData for selected storage type', () => {
+    component.allData = [
+      { group: 'Block', value: 10 },
+      { group: 'Filesystem', value: 20 }
+    ];
+
+    component.selectedStorageType = 'Block';
+    (component as any).setChartData();
+
+    expect(component.displayData.length).toBe(1);
+    expect(component.displayData[0].group).toBe('Block');
+  });
+
+  it('should show all data when ALL selected', () => {
+    component.allData = [
+      { group: 'Block', value: 10 },
+      { group: 'Filesystem', value: 20 }
+    ];
+
+    component.selectedStorageType = 'All';
+    (component as any).setChartData();
+
+    expect(component.displayData.length).toBe(2);
+  });
+
+  // --------------------------------------------------
+  // DROPDOWN
+  // --------------------------------------------------
+
+  it('should update storage type from dropdown selection', () => {
+    component.onStorageTypeSelect({
+      item: { content: 'Block', selected: true }
+    });
+
+    expect(component.selectedStorageType).toBe('Block');
+  });
+
+  it('should auto-select single item if only one exists', () => {
+    component.allData = [{ group: 'Block', value: 10 }];
+
+    (component as any).setDropdownItemsAndStorageType();
+
+    expect(component.selectedStorageType).toBe('Block');
+    expect(component.dropdownItems.length).toBe(1);
+  });
+
+  it('should reset to ALL if previous selection missing', () => {
+    component.selectedStorageType = 'Block';
+
+    component.allData = [
+      { group: 'Filesystem', value: 20 },
+      { group: 'Object', value: 30 }
+    ];
+
+    (component as any).setDropdownItemsAndStorageType();
+
+    expect(component.selectedStorageType).toBe('All');
+  });
+
+  // --------------------------------------------------
+  // DESTROY
+  // --------------------------------------------------
+
+  it('should clean up on destroy', () => {
+    const nextSpy = jest.spyOn((component as any).destroy$, 'next');
+    const completeSpy = jest.spyOn((component as any).destroy$, 'complete');
+
+    component.ngOnDestroy();
+
+    expect(nextSpy).toHaveBeenCalled();
+    expect(completeSpy).toHaveBeenCalled();
+  });
 });
index 0fe568b44db6b9a078d2639eac8d258a975fad38..31fbffefaa5ecf26312191e354375d9380fe8c4b 100644 (file)
@@ -9,8 +9,7 @@ describe('ProductiveCardComponent', () => {
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      declarations: [ProductiveCardComponent],
-      imports: [GridModule, LayerModule, TilesModule]
+      imports: [ProductiveCardComponent, GridModule, LayerModule, TilesModule]
     }).compileComponents();
 
     fixture = TestBed.createComponent(ProductiveCardComponent);