diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 81e93b4b6..f7a06b07b 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -59,6 +59,7 @@ import { selectIsOpenStartTestrun, selectIsOpenWaitSnackBar, selectMenuOpened, + selectReports, selectStatus, selectSystemStatus, } from './store/selectors'; @@ -109,6 +110,7 @@ describe('AppComponent', () => { 'testrunInProgress', 'fetchProfiles', 'fetchCertificates', + 'getHistory', ]); mockService.fetchCertificates.and.returnValue(of([])); @@ -159,6 +161,7 @@ describe('AppComponent', () => { { selector: selectSystemStatus, value: null }, { selector: selectIsOpenStartTestrun, value: false }, { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectReports, value: [] }, ], }), { provide: FocusManagerService, useValue: mockFocusManagerService }, diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 341f6bab5..03e6beeb2 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -80,6 +80,7 @@ export class AppComponent { this.appStore.getDevices(); this.appStore.getRiskProfiles(); this.appStore.getSystemStatus(); + this.appStore.getReports(); this.matIconRegistry.addSvgIcon( 'devices', this.domSanitizer.bypassSecurityTrustResourceUrl(DEVICES_LOGO_URL) diff --git a/modules/ui/src/app/app.store.spec.ts b/modules/ui/src/app/app.store.spec.ts index 2bdf63195..933ee06ec 100644 --- a/modules/ui/src/app/app.store.spec.ts +++ b/modules/ui/src/app/app.store.spec.ts @@ -32,6 +32,7 @@ import { TestRunService } from './services/test-run.service'; import SpyObj = jasmine.SpyObj; import { device } from './mocks/device.mock'; import { + fetchReports, fetchRiskProfiles, fetchSystemStatus, setDevices, @@ -226,5 +227,13 @@ describe('AppStore', () => { ).toHaveBeenCalled(); })); }); + + describe('getReports', () => { + it('should dispatch fetchReports', () => { + appStore.getReports(); + + expect(store.dispatch).toHaveBeenCalledWith(fetchReports()); + }); + }); }); }); diff --git a/modules/ui/src/app/app.store.ts b/modules/ui/src/app/app.store.ts index 9bd8dcff4..cf21d98d8 100644 --- a/modules/ui/src/app/app.store.ts +++ b/modules/ui/src/app/app.store.ts @@ -36,6 +36,7 @@ import { setIsOpenStartTestrun, fetchSystemStatus, fetchRiskProfiles, + fetchReports, } from './store/actions'; import { TestrunStatus } from './model/testrun-status'; import { SettingMissedError, SystemInterfaces } from './model/setting'; @@ -150,6 +151,14 @@ export class AppStore extends ComponentStore { ); }); + getReports = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + this.store.dispatch(fetchReports()); + }) + ); + }); + constructor( private store: Store, private testRunService: TestRunService, diff --git a/modules/ui/src/app/mocks/reports.mock.ts b/modules/ui/src/app/mocks/reports.mock.ts index 0cfb39420..eb70b0b58 100644 --- a/modules/ui/src/app/mocks/reports.mock.ts +++ b/modules/ui/src/app/mocks/reports.mock.ts @@ -43,9 +43,6 @@ export const HISTORY_AFTER_REMOVE = [ report: 'https://api.testrun.io/report.pdf', started: '2023-06-23T10:11:00.123Z', finished: '2023-06-23T10:17:10.123Z', - deviceFirmware: '1.2.2', - deviceInfo: 'Delta 03-DIN-SRC', - duration: '06m 10s', }, ]; diff --git a/modules/ui/src/app/mocks/testrun.mock.ts b/modules/ui/src/app/mocks/testrun.mock.ts index 0572e79c0..bb588634c 100644 --- a/modules/ui/src/app/mocks/testrun.mock.ts +++ b/modules/ui/src/app/mocks/testrun.mock.ts @@ -65,7 +65,7 @@ const PROGRESS_DATA_RESPONSE = ( status: string, finished: string | null, tests: TestsData | IResult[], - report?: string + report: string = '' ) => { return { status, diff --git a/modules/ui/src/app/model/testrun-status.ts b/modules/ui/src/app/model/testrun-status.ts index 2ac908185..59fb1e80f 100644 --- a/modules/ui/src/app/model/testrun-status.ts +++ b/modules/ui/src/app/model/testrun-status.ts @@ -22,7 +22,7 @@ export interface TestrunStatus { started: string | null; finished: string | null; tests?: TestsResponse; - report?: string; + report: string; } export interface HistoryTestrun extends TestrunStatus { @@ -85,6 +85,19 @@ export interface StatusResultClassName { grey: boolean; } +export const IDLE_STATUS = { + status: StatusOfTestrun.Idle, + device: {} as IDevice, + started: null, + finished: null, + report: '', + mac_addr: '', + tests: { + total: 0, + results: [], + }, +} as TestrunStatus; + export type TestrunStatusKey = keyof typeof StatusOfTestrun; export type TestrunStatusValue = (typeof StatusOfTestrun)[TestrunStatusKey]; export type TestResultKey = keyof typeof StatusOfTestResult; diff --git a/modules/ui/src/app/pages/reports/reports-routing.module.ts b/modules/ui/src/app/pages/reports/reports-routing.module.ts index d5542172f..645a8bb52 100644 --- a/modules/ui/src/app/pages/reports/reports-routing.module.ts +++ b/modules/ui/src/app/pages/reports/reports-routing.module.ts @@ -15,7 +15,7 @@ */ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { ReportsComponent } from './reportscomponent'; +import { ReportsComponent } from './reports.component'; const routes: Routes = [{ path: '', component: ReportsComponent }]; diff --git a/modules/ui/src/app/pages/reports/reports.component.spec.ts b/modules/ui/src/app/pages/reports/reports.component.spec.ts index 19a0a827d..0e0fc7b1a 100644 --- a/modules/ui/src/app/pages/reports/reports.component.spec.ts +++ b/modules/ui/src/app/pages/reports/reports.component.spec.ts @@ -15,7 +15,7 @@ */ import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; -import { ReportsComponent } from './reportscomponent'; +import { ReportsComponent } from './reports.component'; import { TestRunService } from '../../services/test-run.service'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ReportsModule } from './reports.module'; @@ -112,12 +112,6 @@ describe('ReportsComponent', () => { }); describe('ngOnInit', () => { - it('should set dataSource data', () => { - component.ngOnInit(); - - expect(mockReportsStore.getHistory).toHaveBeenCalled(); - }); - it('should update sort', fakeAsync(() => { const sort = new MatSort(); component.sort = sort; diff --git a/modules/ui/src/app/pages/reports/reportscomponent.ts b/modules/ui/src/app/pages/reports/reports.component.ts similarity index 99% rename from modules/ui/src/app/pages/reports/reportscomponent.ts rename to modules/ui/src/app/pages/reports/reports.component.ts index e390f0e06..9d5a5914d 100644 --- a/modules/ui/src/app/pages/reports/reportscomponent.ts +++ b/modules/ui/src/app/pages/reports/reports.component.ts @@ -56,7 +56,6 @@ export class ReportsComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.store.getHistory(); this.store.updateSort(this.sort); } diff --git a/modules/ui/src/app/pages/reports/reports.module.ts b/modules/ui/src/app/pages/reports/reports.module.ts index f218780c5..18db8a27f 100644 --- a/modules/ui/src/app/pages/reports/reports.module.ts +++ b/modules/ui/src/app/pages/reports/reports.module.ts @@ -15,7 +15,7 @@ */ import { NgModule } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; -import { ReportsComponent } from './reportscomponent'; +import { ReportsComponent } from './reports.component'; import { ReportsRoutingModule } from './reports-routing.module'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; diff --git a/modules/ui/src/app/pages/reports/reports.store.spec.ts b/modules/ui/src/app/pages/reports/reports.store.spec.ts index e78b40036..248b918c0 100644 --- a/modules/ui/src/app/pages/reports/reports.store.spec.ts +++ b/modules/ui/src/app/pages/reports/reports.store.spec.ts @@ -18,9 +18,8 @@ import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; import { TestBed } from '@angular/core/testing'; import { skip, take } from 'rxjs'; -import { throwError } from 'rxjs/internal/observable/throwError'; import { of } from 'rxjs/internal/observable/of'; -import { DATA_SOURCE_INITIAL_VALUE, ReportsStore } from './reports.store'; +import { ReportsStore } from './reports.store'; import { EMPTY_FILTERS, FILTERS, @@ -29,31 +28,39 @@ import { HISTORY_AFTER_REMOVE, } from '../../mocks/reports.mock'; import { DatePipe } from '@angular/common'; -import { HttpErrorResponse } from '@angular/common/http'; import { MatRow } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; -import { provideMockStore } from '@ngrx/store/testing'; -import { selectRiskProfiles } from '../../store/selectors'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { selectReports, selectRiskProfiles } from '../../store/selectors'; +import { AppState } from '../../store/state'; +import { setReports } from '../../store/actions'; describe('ReportsStore', () => { let reportsStore: ReportsStore; let mockService: SpyObj; + let store: MockStore; beforeEach(() => { - mockService = jasmine.createSpyObj(['getHistory', 'deleteReport']); + mockService = jasmine.createSpyObj(['deleteReport']); TestBed.configureTestingModule({ providers: [ ReportsStore, { provide: TestRunService, useValue: mockService }, provideMockStore({ - selectors: [{ selector: selectRiskProfiles, value: [] }], + selectors: [ + { selector: selectRiskProfiles, value: [] }, + { selector: selectReports, value: [] }, + ], }), DatePipe, ], }); reportsStore = TestBed.inject(ReportsStore); + store = TestBed.inject(MockStore); + + spyOn(store, 'dispatch').and.callFake(() => {}); }); it('should be created', () => { @@ -110,7 +117,7 @@ describe('ReportsStore', () => { }); it('should update dataSource', (done: DoneFn) => { - reportsStore.viewModel$.pipe(skip(2), take(1)).subscribe(store => { + reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); expect(store.dataLoaded).toEqual(true); done(); @@ -133,7 +140,7 @@ describe('ReportsStore', () => { 'report', ], chips: ['chips'], - dataSource: DATA_SOURCE_INITIAL_VALUE, + dataSource: store.dataSource, filterOpened: false, activeFilter: '', filteredValues: { @@ -142,7 +149,7 @@ describe('ReportsStore', () => { results: [], dateRange: '', }, - dataLoaded: false, + dataLoaded: true, selectedRow: null, isFiltersEmpty: true, profiles: [], @@ -154,56 +161,41 @@ describe('ReportsStore', () => { describe('effects', () => { describe('getHistory', () => { - describe('should update store', () => { - it('with empty value if error happens', done => { - mockService.getHistory.and.returnValue( - throwError( - new HttpErrorResponse({ error: { error: 'error' }, status: 500 }) - ) - ); - - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual([]); - done(); - }); + it('should update store', done => { + store.overrideSelector(selectReports, [...HISTORY]); + store.refreshState(); - reportsStore.getHistory(); + reportsStore.viewModel$.pipe(take(1)).subscribe(store => { + expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); + done(); }); - it('with value if not null', done => { - mockService.getHistory.and.returnValue(of([...HISTORY])); - - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); - done(); - }); - - reportsStore.getHistory(); - }); + reportsStore.getHistory(); }); }); describe('deleteReport', () => { it('should update store', done => { mockService.deleteReport.and.returnValue(of(true)); - reportsStore.setHistory([...HISTORY]); - - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual(HISTORY_AFTER_REMOVE); - done(); - }); + store.overrideSelector(selectReports, [...HISTORY]); + store.refreshState(); reportsStore.deleteReport({ mac_addr: '00:1e:42:35:73:c4', started: '2023-06-22T10:11:00.123Z', }); + + expect(store.dispatch).toHaveBeenCalledWith( + setReports({ reports: HISTORY_AFTER_REMOVE }) + ); + done(); }); }); describe('updateSort', () => { it('should update store', done => { const sort = new MatSort(); - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.updateSort(sort); @@ -217,7 +209,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesResults', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ results: ['test2'] } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesResults(['test2']); @@ -235,7 +227,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDeviceInfo', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ deviceInfo: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDeviceInfo('test2'); @@ -253,7 +245,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDeviceFirmware', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ deviceFirmware: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDeviceFirmware('test2'); @@ -271,7 +263,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDateRange', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ dateRange: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDateRange('test2'); @@ -289,7 +281,7 @@ describe('ReportsStore', () => { describe('setFilteredValues', () => { it('should update store', done => { const updatedFilters = { ...EMPTY_FILTERS }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValues(updatedFilters); diff --git a/modules/ui/src/app/pages/reports/reports.store.ts b/modules/ui/src/app/pages/reports/reports.store.ts index ab93a1085..cb1c98fb7 100644 --- a/modules/ui/src/app/pages/reports/reports.store.ts +++ b/modules/ui/src/app/pages/reports/reports.store.ts @@ -4,13 +4,14 @@ import { MatRow, MatTableDataSource } from '@angular/material/table'; import { HistoryTestrun, TestrunStatus } from '../../model/testrun-status'; import { DateRange, Filters } from '../../model/filters'; import { TestRunService } from '../../services/test-run.service'; -import { catchError, EMPTY, exhaustMap } from 'rxjs'; +import { exhaustMap } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; import { DatePipe } from '@angular/common'; import { MatSort } from '@angular/material/sort'; -import { selectRiskProfiles } from '../../store/selectors'; +import { selectReports, selectRiskProfiles } from '../../store/selectors'; import { Store } from '@ngrx/store'; import { AppState } from '../../store/state'; +import { setReports } from '../../store/actions'; export interface ReportsComponentState { displayedColumns: string[]; @@ -28,7 +29,6 @@ export interface ReportsComponentState { export const DATA_SOURCE_INITIAL_VALUE = new MatTableDataSource( [] ); - @Injectable() export class ReportsStore extends ComponentStore { private displayedColumns$ = this.select(state => state.displayedColumns); @@ -40,7 +40,7 @@ export class ReportsStore extends ComponentStore { private dataLoaded$ = this.select(state => state.dataLoaded); private selectedRow$ = this.select(state => state.selectedRow); private isFiltersEmpty$ = this.select(state => state.isFiltersEmpty); - private history$ = this.select(state => state.history); + private history$ = this.store.select(selectReports); private profiles$ = this.store.select(selectRiskProfiles); viewModel$ = this.select({ displayedColumns: this.displayedColumns$, @@ -105,13 +105,6 @@ export class ReportsStore extends ComponentStore { }; }); - setHistory = this.updater((state, history: TestrunStatus[]) => { - return { - ...state, - history, - }; - }); - updateFilteredValues = this.updater((state, filteredValues: Filters) => { return { ...state, @@ -119,28 +112,6 @@ export class ReportsStore extends ComponentStore { }; }); - getHistory = this.effect(trigger$ => { - return trigger$.pipe( - exhaustMap(() => { - return this.testRunService.getHistory().pipe( - withLatestFrom(this.filteredValues$), - tap(([reports, filteredValues]) => { - if (reports) { - this.setDataSource(reports); - this.setFilteredValues(filteredValues); - this.setHistory(reports); - } - }), - catchError(() => { - this.setDataSource([]); - this.setHistory([]); - return EMPTY; - }) - ); - }) - ); - }); - deleteReport = this.effect<{ mac_addr: string; started: string | null; @@ -225,19 +196,27 @@ export class ReportsStore extends ComponentStore { ); }); + getHistory = this.effect(() => { + return this.history$.pipe( + withLatestFrom(this.filteredValues$), + tap(([reports, filteredValues]) => { + this.setDataSource([...reports]); + this?.setFilteredValues(filteredValues); + }) + ); + }); private removeReport( mac_addr: string, started: string | null, current: TestrunStatus[] ) { - const history = current; + const history = [...current]; const idx = history.findIndex( report => report.mac_addr === mac_addr && report.started === started ); if (typeof idx === 'number') { history.splice(idx, 1); - this.setHistory(history); - this.setDataSource(history); + this.store.dispatch(setReports({ reports: history })); } } diff --git a/modules/ui/src/app/store/actions.ts b/modules/ui/src/app/store/actions.ts index 3ca38d16f..27b7cdb99 100644 --- a/modules/ui/src/app/store/actions.ts +++ b/modules/ui/src/app/store/actions.ts @@ -124,3 +124,10 @@ export const setStatus = createAction( export const stopInterval = createAction('[Shared] Stop Interval'); export const fetchRiskProfiles = createAction('[Shared] Fetch risk profiles'); + +export const fetchReports = createAction('[Shared] Fetch reports'); + +export const setReports = createAction( + '[Shared] Set Reports', + props<{ reports: TestrunStatus[] }>() +); diff --git a/modules/ui/src/app/store/effects.spec.ts b/modules/ui/src/app/store/effects.spec.ts index 2ac8b65d2..9f44ae284 100644 --- a/modules/ui/src/app/store/effects.spec.ts +++ b/modules/ui/src/app/store/effects.spec.ts @@ -36,12 +36,22 @@ import { import { device } from '../mocks/device.mock'; import { MOCK_PROGRESS_DATA_CANCELLING, + MOCK_PROGRESS_DATA_COMPLIANT, MOCK_PROGRESS_DATA_IN_PROGRESS, MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE, } from '../mocks/testrun.mock'; -import { fetchSystemStatus, setStatus, setTestrunStatus } from './actions'; +import { + fetchSystemStatus, + setReports, + setStatus, + setTestrunStatus, +} from './actions'; import { NotificationService } from '../services/notification.service'; import { PROFILE_MOCK } from '../mocks/profile.mock'; +import { throwError } from 'rxjs/internal/observable/throwError'; +import { HttpErrorResponse } from '@angular/common/http'; +import { IDLE_STATUS } from '../model/testrun-status'; +import { HISTORY } from '../mocks/reports.mock'; describe('Effects', () => { let actions$ = new Observable(); @@ -64,6 +74,7 @@ describe('Effects', () => { 'testrunInProgress', 'stopTestrun', 'fetchProfiles', + 'getHistory', ]); testRunServiceMock.getSystemInterfaces.and.returnValue(of({})); testRunServiceMock.getSystemConfig.and.returnValue(of({})); @@ -72,6 +83,7 @@ describe('Effects', () => { of(MOCK_PROGRESS_DATA_IN_PROGRESS) ); testRunServiceMock.fetchProfiles.and.returnValue(of([])); + testRunServiceMock.getHistory.and.returnValue(of([])); TestBed.configureTestingModule({ providers: [ @@ -400,4 +412,76 @@ describe('Effects', () => { done(); }); }); + + describe('onFetchReports$', () => { + it(' should call setReports on success', done => { + testRunServiceMock.getHistory.and.returnValue(of([])); + actions$ = of(actions.fetchReports()); + + effects.onFetchReports$.subscribe(action => { + expect(action).toEqual( + actions.setReports({ + reports: [], + }) + ); + done(); + }); + }); + + it('should call setReports with empty array if null is returned', done => { + testRunServiceMock.getHistory.and.returnValue(of(null)); + actions$ = of(actions.fetchReports()); + + effects.onFetchReports$.subscribe(action => { + expect(action).toEqual( + actions.setReports({ + reports: [], + }) + ); + done(); + }); + }); + + it('should call setReports with empty array if error happens', done => { + testRunServiceMock.getHistory.and.returnValue( + throwError( + new HttpErrorResponse({ error: { error: 'error' }, status: 500 }) + ) + ); + actions$ = of(actions.fetchReports()); + + effects.onFetchReports$.subscribe({ + complete: () => { + expect(dispatchSpy).toHaveBeenCalledWith( + setReports({ + reports: [], + }) + ); + done(); + }, + }); + }); + }); + + describe('checkStatusInReports$', () => { + it('should call setTestrunStatus if current test run is completed and not present in reports', done => { + actions$ = of( + actions.setReports({ + reports: HISTORY, + }), + actions.fetchSystemStatusSuccess({ + systemStatus: Object.assign({}, MOCK_PROGRESS_DATA_COMPLIANT, { + mac_addr: '01:02:03:04:05:07', + }), + }) + ); + + effects.checkStatusInReports$.subscribe(action => { + expect(action).toEqual( + actions.setTestrunStatus({ systemStatus: IDLE_STATUS }) + ); + done(); + }); + }); + }); }); diff --git a/modules/ui/src/app/store/effects.ts b/modules/ui/src/app/store/effects.ts index 10b2e4afb..a7bb6848e 100644 --- a/modules/ui/src/app/store/effects.ts +++ b/modules/ui/src/app/store/effects.ts @@ -22,15 +22,31 @@ import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import * as AppActions from './actions'; import { AppState } from './state'; import { TestRunService } from '../services/test-run.service'; -import { filter, combineLatest, interval, Subject, timer, take } from 'rxjs'; +import { + filter, + combineLatest, + interval, + Subject, + timer, + take, + catchError, + EMPTY, +} from 'rxjs'; import { selectIsOpenWaitSnackBar, selectMenuOpened, selectSystemStatus, } from './selectors'; -import { IResult, StatusOfTestrun, TestsData } from '../model/testrun-status'; +import { + IDLE_STATUS, + IResult, + StatusOfTestrun, + TestrunStatus, + TestsData, +} from '../model/testrun-status'; import { fetchSystemStatus, + setReports, setStatus, setTestrunStatus, stopInterval, @@ -260,6 +276,52 @@ export class AppEffects { ); }); + onFetchReports$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.fetchReports), + switchMap(() => + this.testrunService.getHistory().pipe( + map((reports: TestrunStatus[] | null) => { + if (reports !== null) { + return AppActions.setReports({ reports }); + } + return AppActions.setReports({ reports: [] }); + }), + catchError(() => { + this.store.dispatch(setReports({ reports: [] })); + return EMPTY; + }) + ) + ) + ); + }); + + checkStatusInReports$ = createEffect(() => + combineLatest([ + this.actions$.pipe(ofType(AppActions.setReports)), + this.actions$.pipe(ofType(AppActions.fetchSystemStatusSuccess)), + ]).pipe( + filter(([, { systemStatus }]) => { + return ( + systemStatus.status === StatusOfTestrun.Compliant || + systemStatus.status === StatusOfTestrun.NonCompliant || + systemStatus.status === StatusOfTestrun.Error + ); + }), + filter(([{ reports }, { systemStatus }]) => { + return ( + !reports?.some( + report => + report.report === systemStatus.report && + report.started === systemStatus.started && + report.finished === systemStatus.finished + ) || false + ); + }), + map(() => AppActions.setTestrunStatus({ systemStatus: IDLE_STATUS })) + ) + ); + private showSnackBar() { timer(WAIT_TO_OPEN_SNACKBAR_MS) .pipe( diff --git a/modules/ui/src/app/store/reducers.spec.ts b/modules/ui/src/app/store/reducers.spec.ts index ad611e9f9..b136f3abe 100644 --- a/modules/ui/src/app/store/reducers.spec.ts +++ b/modules/ui/src/app/store/reducers.spec.ts @@ -25,6 +25,7 @@ import { setIsOpenAddDevice, setIsOpenStartTestrun, setIsOpenWaitSnackBar, + setReports, setRiskProfiles, setStatus, setTestrunStatus, @@ -35,6 +36,7 @@ import { import { device } from '../mocks/device.mock'; import { MOCK_PROGRESS_DATA_CANCELLING } from '../mocks/testrun.mock'; import { PROFILE_MOCK } from '../mocks/profile.mock'; +import { HISTORY } from '../mocks/reports.mock'; describe('Reducer', () => { describe('unknown action', () => { @@ -258,4 +260,21 @@ describe('Reducer', () => { expect(state).not.toBe(initialState); }); }); + + describe('setReports action', () => { + it('should update state', () => { + const initialState = initialSharedState; + const action = setReports({ + reports: HISTORY, + }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { + ...initialState, + ...{ reports: HISTORY }, + }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); }); diff --git a/modules/ui/src/app/store/reducers.ts b/modules/ui/src/app/store/reducers.ts index 501c231a5..b4ae7898b 100644 --- a/modules/ui/src/app/store/reducers.ts +++ b/modules/ui/src/app/store/reducers.ts @@ -106,6 +106,12 @@ export const sharedReducer = createReducer( ...state, status, }; + }), + on(Actions.setReports, (state, { reports }) => { + return { + ...state, + reports, + }; }) ); diff --git a/modules/ui/src/app/store/selectors.spec.ts b/modules/ui/src/app/store/selectors.spec.ts index e8d31efc8..ac21ceb3e 100644 --- a/modules/ui/src/app/store/selectors.spec.ts +++ b/modules/ui/src/app/store/selectors.spec.ts @@ -27,6 +27,7 @@ import { selectIsOpenStartTestrun, selectIsOpenWaitSnackBar, selectMenuOpened, + selectReports, selectRiskProfiles, selectStatus, selectSystemStatus, @@ -55,6 +56,7 @@ describe('Selectors', () => { systemStatus: null, deviceInProgress: null, status: null, + reports: [], }, }; @@ -127,4 +129,9 @@ describe('Selectors', () => { const result = selectStatus.projector(initialState); expect(result).toEqual(null); }); + + it('should select status', () => { + const result = selectReports.projector(initialState); + expect(result).toEqual([]); + }); }); diff --git a/modules/ui/src/app/store/selectors.ts b/modules/ui/src/app/store/selectors.ts index 2f42db3d6..580f94a8b 100644 --- a/modules/ui/src/app/store/selectors.ts +++ b/modules/ui/src/app/store/selectors.ts @@ -93,3 +93,8 @@ export const selectStatus = createSelector( selectAppState, (state: AppState) => state.shared.status ); + +export const selectReports = createSelector( + selectAppState, + (state: AppState) => state.shared.reports +); diff --git a/modules/ui/src/app/store/state.ts b/modules/ui/src/app/store/state.ts index e2528c5a0..5269c341f 100644 --- a/modules/ui/src/app/store/state.ts +++ b/modules/ui/src/app/store/state.ts @@ -54,6 +54,7 @@ export interface SharedState { isStopTestrun: boolean; isOpenWaitSnackBar: boolean; deviceInProgress: Device | null; + reports: TestrunStatus[]; } export const initialAppComponentState: AppComponentState = { @@ -78,4 +79,5 @@ export const initialSharedState: SharedState = { isOpenStartTestrun: false, systemStatus: null, status: null, + reports: [], };