From 1c16710f05f81943386d89b237435ef926b62ce5 Mon Sep 17 00:00:00 2001 From: Faust1 Date: Wed, 13 Sep 2023 09:35:01 +0200 Subject: [PATCH 1/4] Initialize applications tests --- src/app/applications/index.component.spec.ts | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/app/applications/index.component.spec.ts diff --git a/src/app/applications/index.component.spec.ts b/src/app/applications/index.component.spec.ts new file mode 100644 index 000000000..27de8f00b --- /dev/null +++ b/src/app/applications/index.component.spec.ts @@ -0,0 +1,41 @@ +import { TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; +import { AutoRefreshService } from '@services/auto-refresh.service'; +import { FiltersService } from '@services/filters.service'; +import { IconsService } from '@services/icons.service'; +import { NotificationService } from '@services/notification.service'; +import { ShareUrlService } from '@services/share-url.service'; +import { TasksByStatusService } from '@services/tasks-by-status.service'; +import { IndexComponent } from './index.component'; +import { ApplicationsGrpcService } from './services/applications-grpc.service'; +import { ApplicationsIndexService } from './services/applications-index.service'; + +describe('Application component', () => { + + let component: IndexComponent; + + beforeEach(() => { + component = TestBed.configureTestingModule({ + providers: [ + IndexComponent, + {provide: TasksByStatusService, useValue: {} }, + {provide: NotificationService, useValue: {} }, + {provide: MatDialog, useValue: {} }, + {provide: IconsService, useValue: {} }, + {provide: FiltersService, useValue: {} }, + {provide: DATA_FILTERS_SERVICE, useValue: {} }, + {provide: ShareUrlService, useValue: {} }, + {provide: ApplicationsIndexService, useValue: {} }, + {provide: ApplicationsGrpcService, useValue: {} }, + {provide: AutoRefreshService, useValue: { + createInterval: jest.fn() + } }, + ] + }).inject(IndexComponent); + }); + + it('Should run', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file From 37a9ed294e405332767630856a47614150e493a2 Mon Sep 17 00:00:00 2001 From: Faust1 Date: Tue, 10 Oct 2023 17:13:23 +0200 Subject: [PATCH 2/4] added some mocks --- src/app/applications/index.component.spec.ts | 53 ++++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/src/app/applications/index.component.spec.ts b/src/app/applications/index.component.spec.ts index 27de8f00b..ceafccf7b 100644 --- a/src/app/applications/index.component.spec.ts +++ b/src/app/applications/index.component.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; +import { DataFilterService } from '@app/types/filter-definition'; import { AutoRefreshService } from '@services/auto-refresh.service'; import { FiltersService } from '@services/filters.service'; import { IconsService } from '@services/icons.service'; @@ -15,20 +16,54 @@ describe('Application component', () => { let component: IndexComponent; + const mockApplicationIndexService = { + availableColumsn: [], + restoreColumns: jest.fn(), + saveColumns: jest.fn(), + resetColumns: jest.fn(), + restoreOptions: jest.fn(), + restoreIntervalValue: jest.fn(), + saveIntervalValue: jest.fn(), + saveOptions: jest.fn(), + columnToLabel: jest.fn(), + isActionsColumn: jest.fn(), + isCountColumn: jest.fn(), + isSimpleColumn: jest.fn(), + isNotSortableColumn: jest.fn() + }; + + const mockShareUrlService = { + generateSharableUrl: jest.fn() + }; + + const mockApplicationsFilterService = { + restoreFilters: jest.fn() + }; + + const mockTasksByStatusService = { + restoreStatuses: jest.fn(), + saveStatuses: jest.fn() + }; + + const mockNotificationService = { + error: jest.fn() + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ IndexComponent, - {provide: TasksByStatusService, useValue: {} }, - {provide: NotificationService, useValue: {} }, + {provide: TasksByStatusService, useValue: mockTasksByStatusService }, + {provide: NotificationService, useValue: mockNotificationService }, {provide: MatDialog, useValue: {} }, - {provide: IconsService, useValue: {} }, - {provide: FiltersService, useValue: {} }, - {provide: DATA_FILTERS_SERVICE, useValue: {} }, - {provide: ShareUrlService, useValue: {} }, - {provide: ApplicationsIndexService, useValue: {} }, - {provide: ApplicationsGrpcService, useValue: {} }, - {provide: AutoRefreshService, useValue: { + IconsService, + FiltersService, + DataFilterService, + { provide: DATA_FILTERS_SERVICE, useValue: mockApplicationsFilterService }, + { provide: ShareUrlService, useValue: mockShareUrlService }, + { provide: ApplicationsIndexService, useValue: mockApplicationIndexService }, + { provide: ApplicationsGrpcService, useValue: {} }, + { provide: AutoRefreshService, useValue: { createInterval: jest.fn() } }, ] From 1045c31408907e32fbfee2e349ccedc6fdbf8b24 Mon Sep 17 00:00:00 2001 From: Faust1 Date: Wed, 11 Oct 2023 10:06:26 +0200 Subject: [PATCH 3/4] added test --- src/app/applications/index.component.spec.ts | 33 +++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/app/applications/index.component.spec.ts b/src/app/applications/index.component.spec.ts index ceafccf7b..9c5f2c34d 100644 --- a/src/app/applications/index.component.spec.ts +++ b/src/app/applications/index.component.spec.ts @@ -37,7 +37,9 @@ describe('Application component', () => { }; const mockApplicationsFilterService = { - restoreFilters: jest.fn() + restoreFilters: jest.fn(), + saveFilters: jest.fn(), + resetFilters: jest.fn(), }; const mockTasksByStatusService = { @@ -49,6 +51,15 @@ describe('Application component', () => { error: jest.fn() }; + const mockGrpcApplicationsService = { + list$: jest.fn() + }; + + const mockAutoRefreshService = { + createInterval: jest.fn(), + autoRefreshTooltip: jest.fn() + }; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ @@ -62,15 +73,27 @@ describe('Application component', () => { { provide: DATA_FILTERS_SERVICE, useValue: mockApplicationsFilterService }, { provide: ShareUrlService, useValue: mockShareUrlService }, { provide: ApplicationsIndexService, useValue: mockApplicationIndexService }, - { provide: ApplicationsGrpcService, useValue: {} }, - { provide: AutoRefreshService, useValue: { - createInterval: jest.fn() - } }, + { provide: ApplicationsGrpcService, useValue: mockGrpcApplicationsService }, + { provide: AutoRefreshService, useValue: mockAutoRefreshService }, ] }).inject(IndexComponent); + + component.paginator.pageIndex = 1; + component.paginator.pageSize = 25; }); it('Should run', () => { expect(component).toBeTruthy(); }); + + it('should init', () => { + component.ngOnInit(); + expect(mockApplicationIndexService.restoreColumns).toHaveBeenCalled(); + expect(mockApplicationIndexService.restoreOptions).toHaveBeenCalled(); + expect(mockApplicationsFilterService.restoreFilters).toHaveBeenCalled(); + expect(mockApplicationIndexService.restoreIntervalValue).toHaveBeenCalled(); + expect(mockShareUrlService.generateSharableUrl).toHaveBeenCalledWith(component.options, component.filters); + expect(mockTasksByStatusService.restoreStatuses).toHaveBeenCalledWith('applications'); + expect(component.availableColumns).toEqual(); + }); }); \ No newline at end of file From fa255926f3b8e36b70c92a0a25acbd4492d5563f Mon Sep 17 00:00:00 2001 From: Faust1 Date: Wed, 11 Oct 2023 16:26:09 +0200 Subject: [PATCH 4/4] added all tests for application component --- src/app/applications/index.component.spec.ts | 357 ++++++++++++++++++- src/app/applications/index.component.ts | 2 +- 2 files changed, 341 insertions(+), 18 deletions(-) diff --git a/src/app/applications/index.component.spec.ts b/src/app/applications/index.component.spec.ts index 9c5f2c34d..dc8b1e97e 100644 --- a/src/app/applications/index.component.spec.ts +++ b/src/app/applications/index.component.spec.ts @@ -1,7 +1,14 @@ +import { TaskStatus } from '@aneoconsultingfr/armonik.api.angular'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort, Sort } from '@angular/material/sort'; +import { BehaviorSubject, Subject, throwError } from 'rxjs'; import { DATA_FILTERS_SERVICE } from '@app/tokens/filters.token'; +import { TaskStatusColored } from '@app/types/dialog'; import { DataFilterService } from '@app/types/filter-definition'; +import { FiltersOr } from '@app/types/filters'; import { AutoRefreshService } from '@services/auto-refresh.service'; import { FiltersService } from '@services/filters.service'; import { IconsService } from '@services/icons.service'; @@ -11,33 +18,81 @@ import { TasksByStatusService } from '@services/tasks-by-status.service'; import { IndexComponent } from './index.component'; import { ApplicationsGrpcService } from './services/applications-grpc.service'; import { ApplicationsIndexService } from './services/applications-index.service'; +import { ApplicationRawColumnKey, ApplicationRawFilter } from './types'; describe('Application component', () => { let component: IndexComponent; + const dataSample = { + total: 3, + applications: [ + {id: 'abc', sessions: [], partitionsId: []}, + {id: 'def', sessions: [], partitionsId: []}, + {id: 'ghi', sessions: [], partitionsId: []}, + ] + }; + + const sampleFiltersOr: FiltersOr = [ + [ + { + field: 1, + for: 'root', + operator: 1, + value: 2 + }, + { + field: 2, + for: 'root', + operator: 1, + value: 'value' + } + ] + ]; + + const options = { + pageIndex: 1, + pageSize: 25, + sort: { + active: 'sessions', + direction: 'asc' + } + }; + + const intervalRefreshSubject = new Subject(); + const mockApplicationIndexService = { - availableColumsn: [], + availableColumns: ['name', 'namespace', 'service', 'version', 'actions', 'count'], + columnsLabels: { + name: $localize`Name`, + namespace: $localize`Namespace`, + service: $localize`Service`, + version: $localize`Version`, + count: $localize`Tasks by Status`, + actions: $localize`Actions`, + }, restoreColumns: jest.fn(), saveColumns: jest.fn(), resetColumns: jest.fn(), - restoreOptions: jest.fn(), - restoreIntervalValue: jest.fn(), - saveIntervalValue: jest.fn(), - saveOptions: jest.fn(), columnToLabel: jest.fn(), isActionsColumn: jest.fn(), isCountColumn: jest.fn(), isSimpleColumn: jest.fn(), - isNotSortableColumn: jest.fn() + isNotSortableColumn: jest.fn(), + restoreOptions: jest.fn(), + restoreIntervalValue: jest.fn(), + saveIntervalValue: jest.fn(), + saveOptions: jest.fn(), }; const mockShareUrlService = { - generateSharableUrl: jest.fn() + generateSharableURL: jest.fn() }; const mockApplicationsFilterService = { - restoreFilters: jest.fn(), + restoreFilters: jest.fn(() => { + return sampleFiltersOr; + }), saveFilters: jest.fn(), resetFilters: jest.fn(), }; @@ -56,17 +111,29 @@ describe('Application component', () => { }; const mockAutoRefreshService = { - createInterval: jest.fn(), - autoRefreshTooltip: jest.fn() + autoRefreshTooltip: jest.fn(), + createInterval: jest.fn(() => intervalRefreshSubject), }; + let dialogSubject: BehaviorSubject; + beforeEach(() => { component = TestBed.configureTestingModule({ providers: [ IndexComponent, {provide: TasksByStatusService, useValue: mockTasksByStatusService }, {provide: NotificationService, useValue: mockNotificationService }, - {provide: MatDialog, useValue: {} }, + {provide: MatDialog, useValue: + { + open: () => { + return { + afterClosed: () => { + return dialogSubject; + } + }; + } + } + }, IconsService, FiltersService, DataFilterService, @@ -78,22 +145,278 @@ describe('Application component', () => { ] }).inject(IndexComponent); - component.paginator.pageIndex = 1; - component.paginator.pageSize = 25; + component.paginator = { + pageIndex: 1, + pageSize: 25, + page: new Subject() + } as unknown as MatPaginator; + + component.sort = new MatSort(); + component.sort.active = 'sessions'; + component.sort.direction = 'asc'; + + component.ngOnInit(); + component.ngAfterViewInit(); + + component.displayedColumns = ['name', 'namespace', 'service', 'version', 'actions', 'count']; }); it('Should run', () => { expect(component).toBeTruthy(); }); - it('should init', () => { - component.ngOnInit(); + it('should restore on init', () => { expect(mockApplicationIndexService.restoreColumns).toHaveBeenCalled(); expect(mockApplicationIndexService.restoreOptions).toHaveBeenCalled(); expect(mockApplicationsFilterService.restoreFilters).toHaveBeenCalled(); expect(mockApplicationIndexService.restoreIntervalValue).toHaveBeenCalled(); - expect(mockShareUrlService.generateSharableUrl).toHaveBeenCalledWith(component.options, component.filters); + expect(mockShareUrlService.generateSharableURL).toHaveBeenCalledWith(component.options, component.filters); expect(mockTasksByStatusService.restoreStatuses).toHaveBeenCalledWith('applications'); - expect(component.availableColumns).toEqual(); + expect(component.availableColumns).toBe(mockApplicationIndexService.availableColumns); + }); + + describe('ngAfterViewInit', () => { + mockGrpcApplicationsService.list$.mockImplementation(() => { + return new BehaviorSubject(dataSample); + }); + + describe('on sort change', () => { + + const sort: Sort = { + active: 'id', + direction: 'asc' + }; + + it('should reset pageIndex', () => { + component.sort.sortChange.next(sort); + expect(component.paginator.pageIndex).toEqual(0); + }); + + it('should load data', () => { + component.sort.sortChange.next(sort); + expect(mockGrpcApplicationsService.list$).toHaveBeenCalledWith(options, sampleFiltersOr); + expect(component.total).toEqual(3); + expect(component.data).toEqual(dataSample.applications); + }); + }); + }); + + it('should load data on page change', () => { + component.paginator.page.next({ + pageIndex: 2, + previousPageIndex: 1, + pageSize: 25, + length: 10 + }); + expect(mockGrpcApplicationsService.list$).toHaveBeenCalledWith(options, sampleFiltersOr); + expect(component.total).toEqual(3); + expect(component.data).toEqual(dataSample.applications); + }); + + it('should load data on user refresh', () => { + component.refresh.next(); + expect(mockGrpcApplicationsService.list$).toHaveBeenCalledWith(options, sampleFiltersOr); + expect(component.total).toEqual(3); + expect(component.data).toEqual(dataSample.applications); + }); + + it('should load data on interval refresh', () => { + intervalRefreshSubject.next(1); + expect(mockGrpcApplicationsService.list$).toHaveBeenCalledWith(options, sampleFiltersOr); + expect(component.total).toEqual(3); + expect(component.data).toEqual(dataSample.applications); + }); + + it('should catch the error and have empty data', () => { + mockGrpcApplicationsService.list$.mockImplementationOnce(() => { + return throwError(() => new Error('Test error')); + }); + + // To prevent the error to be printed in the console + const mockConsole = jest.spyOn(console, 'error'); + mockConsole.mockImplementationOnce(() => {}); + + component.refresh.next(); + expect(mockNotificationService.error).toHaveBeenCalledWith('Unable to fetch applications'); + expect(component.data).toEqual([]); + expect(component.total).toEqual(0); + }); + + it('should return column labels', () => { + expect(component.columnsLabels()).toEqual(mockApplicationIndexService.columnsLabels); + }); + + it('should return the label of a column', () => { + component.columnToLabel('name'); + expect(mockApplicationIndexService.columnToLabel).toHaveBeenCalledWith('name'); + }); + + it('should check if a column is an action', () => { + component.isActionsColumn('actions'); + expect(mockApplicationIndexService.isActionsColumn).toHaveBeenCalledWith('actions'); + }); + + it('should check if a column is a count column', () => { + component.isCountColumn('count'); + expect(mockApplicationIndexService.isCountColumn).toHaveBeenCalledWith('count'); + }); + + it('should check if a column is a simple column', () => { + component.isSimpleColumn('name'); + expect(mockApplicationIndexService.isSimpleColumn).toHaveBeenCalledWith('name'); + }); + + it('should check if a column is a sortable column', () => { + component.isNotSortableColumn('actions'); + expect(mockApplicationIndexService.isNotSortableColumn).toHaveBeenCalledWith('actions'); + }); + + it('should get page icon', () => { + expect(component.getPageIcon('applications')).toEqual('apps'); + }); + + it('should get required icons', () => { + expect(component.getIcon('tune')).toEqual('tune'); + expect(component.getIcon('more')).toEqual('more_vert'); + expect(component.getIcon('view')).toEqual('visibility'); + }); + + it('should refresh', () => { + const spyRefresh = jest.spyOn(component.refresh, 'next'); + component.onRefresh(); + expect(spyRefresh).toHaveBeenCalled(); + }); + + describe('onIntervalValueChange', () => { + it('should call the application index service', () => { + component.onIntervalValueChange(1); + expect(mockApplicationIndexService.saveIntervalValue).toHaveBeenCalled(); + }); + + it('should change the interval value', () => { + const spyRefresh = jest.spyOn(component.interval, 'next'); + component.onIntervalValueChange(10); + expect(spyRefresh).toHaveBeenCalledWith(10); + }); + + it('should stop the interval value', () => { + const spyStopRefresh = jest.spyOn(component.stopInterval, 'next'); + component.onIntervalValueChange(0); + expect(spyStopRefresh).toHaveBeenCalled(); + }); + }); + + it('should change columns', () => { + const newColumns: ApplicationRawColumnKey[] = ['name', 'count', 'service']; + component.onColumnsChange(newColumns); + expect(component.displayedColumns).toEqual(newColumns); + expect(mockApplicationIndexService.saveColumns).toHaveBeenCalledWith(newColumns); + }); + + it('should reset columns', () => { + mockApplicationIndexService.resetColumns.mockImplementationOnce(() => mockApplicationIndexService.availableColumns); + component.onColumnsReset(); + expect(component.displayedColumns).toEqual(mockApplicationIndexService.availableColumns); + expect(mockApplicationIndexService.resetColumns).toHaveBeenCalled(); + }); + + it('should update filters', () => { + const newFilterOr: ApplicationRawFilter = [[{ + field: 3, + for: 'root', + operator: 4, + value: 'new value' + }]]; + const spyRefresh = jest.spyOn(component.refresh, 'next'); + component.onFiltersChange(newFilterOr); + + expect(mockApplicationsFilterService.saveFilters).toHaveBeenCalledWith(newFilterOr); + expect(component.paginator.pageIndex).toEqual(0); + expect(spyRefresh).toHaveBeenCalled(); + expect(component.filters).toEqual(newFilterOr); + }); + + it('should reset filters', () => { + mockApplicationsFilterService.resetFilters.mockImplementationOnce(() => []); + component.onFiltersReset(); + expect(mockApplicationsFilterService.resetFilters).toHaveBeenCalled(); + expect(component.paginator.pageIndex).toEqual(0); + expect(component.filters).toEqual([]); + }); + + it('should give the tooltip', () => { + component.intervalValue = 13; + component.autoRefreshTooltip(); + expect(mockAutoRefreshService.autoRefreshTooltip).toHaveBeenCalledWith(13); + }); + + it('should change column order', () => { + const event = { + previousIndex: 0, + currentIndex: 1 + } as unknown as CdkDragDrop; + component.onDrop(event); + expect(mockApplicationIndexService.saveColumns).toHaveBeenCalledWith(component.displayedColumns); + expect(component.displayedColumns).toEqual(['namespace', 'name', 'service', 'version', 'actions', 'count']); + }); + + it('should stop the auto-refresh', () => { + const spyStopRefresh = jest.spyOn(component.stopInterval, 'next'); + component.intervalValue = 0; + component.handleAutoRefreshStart(); + expect(spyStopRefresh).toHaveBeenCalled(); + }); + + it('should count tasks by status of the filters', () => { + expect(component.countTasksByStatusFilters('unified_api', '1.0.0')) + .toEqual([[ + { + for: 'options', + field: 5, + value: 'unified_api', + operator: 0 + }, + { + for: 'options', + field: 6, + value: '1.0.0', + operator: 0 + } + ]]); + }); + + it('should create the query params of the tasks by status', () => { + expect(component.createTasksByStatusQueryParams('name', 'version')) + .toEqual({ + ['0-options-5-0']: 'name', + ['0-options-6-0']: 'version' + }); + }); + + it('should create the query params to view sessions', () => { + expect(component.createViewSessionsQueryParams('name', 'version')) + .toEqual({ + ['0-options-5-0']: 'name', + ['0-options-6-0']: 'version' + }); + }); + + it('should permit to personalize tasks by status', () => { + dialogSubject = new BehaviorSubject([{ + color: 'green', + status: TaskStatus.TASK_STATUS_COMPLETED + }]); + component.personalizeTasksByStatus(); + expect(component.tasksStatusesColored).toEqual([{ + color: 'green', + status: TaskStatus.TASK_STATUS_COMPLETED + }]); + expect(mockTasksByStatusService.saveStatuses).toHaveBeenCalled(); + }); + + it('should not personalize if there is no result', () => { + dialogSubject = new BehaviorSubject(undefined); + component.personalizeTasksByStatus(); + expect(mockTasksByStatusService.saveStatuses).toHaveBeenCalledTimes(0); }); }); \ No newline at end of file diff --git a/src/app/applications/index.component.ts b/src/app/applications/index.component.ts index e2d1c9186..cfad71272 100644 --- a/src/app/applications/index.component.ts +++ b/src/app/applications/index.component.ts @@ -361,7 +361,7 @@ export class IndexComponent implements OnInit, AfterViewInit, OnDestroy { } onColumnsChange(data: ApplicationRawColumnKey[]) { - this.displayedColumns = [...data]; + this.displayedColumns = data; this._applicationsIndexService.saveColumns(data); }