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);
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();
+ });
});
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();
+ });
});