diff --git a/modules/ui/src/app/components/snack-bar/snack-bar.component.html b/modules/ui/src/app/components/snack-bar/snack-bar.component.html new file mode 100644 index 000000000..716198299 --- /dev/null +++ b/modules/ui/src/app/components/snack-bar/snack-bar.component.html @@ -0,0 +1,39 @@ + +
+
+

The Waiting for Device stage is taking more than one minute.

+

+ Please check device connection or stop and update system configuration. +

+
+ + + + +
diff --git a/modules/ui/src/app/components/snack-bar/snack-bar.component.scss b/modules/ui/src/app/components/snack-bar/snack-bar.component.scss new file mode 100644 index 000000000..c3772e863 --- /dev/null +++ b/modules/ui/src/app/components/snack-bar/snack-bar.component.scss @@ -0,0 +1,31 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../theming/colors'; + +.snack-bar-container { + display: flex; + + .snack-bar-label p { + margin: 0; + } + + .snack-bar-actions button.action-btn { + color: $blue-300; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + } +} diff --git a/modules/ui/src/app/components/snack-bar/snack-bar.component.spec.ts b/modules/ui/src/app/components/snack-bar/snack-bar.component.spec.ts new file mode 100644 index 000000000..76013d1f8 --- /dev/null +++ b/modules/ui/src/app/components/snack-bar/snack-bar.component.spec.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SnackBarComponent } from './snack-bar.component'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { AppState } from '../../store/state'; +import { MatSnackBarRef } from '@angular/material/snack-bar'; +import { setIsOpenWaitSnackBar, setIsStopTestrun } from '../../store/actions'; + +describe('SnackBarComponent', () => { + let component: SnackBarComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let store: MockStore; + + const MatSnackBarRefMock = { + open: () => ({}), + dismiss: () => ({}), + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SnackBarComponent], + providers: [ + { provide: MatSnackBarRef, useValue: MatSnackBarRefMock }, + provideMockStore({}), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SnackBarComponent); + component = fixture.componentInstance; + store = TestBed.inject(MockStore); + compiled = fixture.nativeElement as HTMLElement; + spyOn(store, 'dispatch').and.callFake(() => {}); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should dispatch setIsStopTestrun action', () => { + const actionBtnStop = compiled.querySelector( + '.action-btn.stop' + ) as HTMLButtonElement; + + actionBtnStop.click(); + + expect(store.dispatch).toHaveBeenCalledWith( + setIsStopTestrun({ isStopTestrun: true }) + ); + }); + + it('should dispatch setIsOpenWaitSnackBar action', () => { + const actionBtnWait = compiled.querySelector( + '.action-btn.wait' + ) as HTMLButtonElement; + + actionBtnWait.click(); + + expect(store.dispatch).toHaveBeenCalledWith( + setIsOpenWaitSnackBar({ isOpenWaitSnackBar: false }) + ); + }); +}); diff --git a/modules/ui/src/app/components/snack-bar/snack-bar.component.ts b/modules/ui/src/app/components/snack-bar/snack-bar.component.ts new file mode 100644 index 000000000..aa19f971b --- /dev/null +++ b/modules/ui/src/app/components/snack-bar/snack-bar.component.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { + MatSnackBarAction, + MatSnackBarActions, + MatSnackBarLabel, + MatSnackBarRef, +} from '@angular/material/snack-bar'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../store/state'; +import { setIsOpenWaitSnackBar, setIsStopTestrun } from '../../store/actions'; + +@Component({ + selector: 'app-snack-bar', + standalone: true, + imports: [ + MatButtonModule, + MatSnackBarLabel, + MatSnackBarActions, + MatSnackBarAction, + ], + templateUrl: './snack-bar.component.html', + styleUrl: './snack-bar.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SnackBarComponent { + snackBarRef = inject(MatSnackBarRef); + constructor(private store: Store) {} + + wait(): void { + this.snackBarRef.dismiss(); + this.store.dispatch(setIsOpenWaitSnackBar({ isOpenWaitSnackBar: false })); + } + + stop(): void { + this.store.dispatch(setIsStopTestrun({ isStopTestrun: true })); + } +} diff --git a/modules/ui/src/app/pages/certificates/certificates.component.spec.ts b/modules/ui/src/app/pages/certificates/certificates.component.spec.ts index 16c5b32dc..0a50f6b9d 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.spec.ts +++ b/modules/ui/src/app/pages/certificates/certificates.component.spec.ts @@ -18,6 +18,7 @@ import { fakeAsync, flush, TestBed, + tick, } from '@angular/core/testing'; import { CertificatesComponent } from './certificates.component'; @@ -30,6 +31,7 @@ import { of } from 'rxjs'; import { MatDialogRef } from '@angular/material/dialog'; import { DeleteFormComponent } from '../../components/delete-form/delete-form.component'; import { TestRunService } from '../../services/test-run.service'; +import { NotificationService } from '../../services/notification.service'; describe('CertificatesComponent', () => { let component: CertificatesComponent; @@ -37,6 +39,9 @@ describe('CertificatesComponent', () => { let mockService: SpyObj; let fixture: ComponentFixture; + const notificationServiceMock: jasmine.SpyObj = + jasmine.createSpyObj(['notify']); + beforeEach(async () => { mockService = jasmine.createSpyObj([ 'fetchCertificates', @@ -53,6 +58,7 @@ describe('CertificatesComponent', () => { providers: [ { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, { provide: TestRunService, useValue: mockService }, + { provide: NotificationService, useValue: notificationServiceMock }, ], }).compileComponents(); @@ -119,8 +125,10 @@ describe('CertificatesComponent', () => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), } as MatDialogRef); + tick(); component.deleteCertificate(certificate.name); + tick(); expect(openSpy).toHaveBeenCalledWith(DeleteFormComponent, { ariaLabel: 'Delete certificate', diff --git a/modules/ui/src/app/pages/testrun/progress.component.spec.ts b/modules/ui/src/app/pages/testrun/progress.component.spec.ts index 5e9506e1b..4c22e3cfd 100644 --- a/modules/ui/src/app/pages/testrun/progress.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/progress.component.spec.ts @@ -49,12 +49,15 @@ import { selectDevices, selectHasDevices, selectIsOpenStartTestrun, + selectIsOpenWaitSnackBar, + selectIsStopTestrun, selectIsTestrunStarted, selectSystemStatus, } from '../../store/selectors'; import { TestrunStore } from './testrun.store'; import { setTestrunStatus } from '../../store/actions'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NotificationService } from '../../services/notification.service'; describe('ProgressComponent', () => { let component: ProgressComponent; @@ -77,6 +80,13 @@ describe('ProgressComponent', () => { ['setLoading', 'getLoading'] ); + const notificationServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('NotificationService', [ + 'dismissWithTimout', + 'dismissSnackBar', + 'openSnackBar', + ]); + const stateServiceMock: jasmine.SpyObj = jasmine.createSpyObj('stateServiceMock', ['focusFirstElementInContainer']); @@ -97,6 +107,7 @@ describe('ProgressComponent', () => { { provide: TestRunService, useValue: testRunServiceMock }, { provide: FocusManagerService, useValue: stateServiceMock }, { provide: LoaderService, useValue: loaderServiceMock }, + { provide: NotificationService, useValue: notificationServiceMock }, { provide: MatDialogRef, useValue: {}, @@ -106,6 +117,8 @@ describe('ProgressComponent', () => { { selector: selectHasDevices, value: false }, { selector: selectIsOpenStartTestrun, value: false }, { selector: selectIsTestrunStarted, value: false }, + { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectIsStopTestrun, value: false }, { selector: selectSystemStatus, value: MOCK_PROGRESS_DATA_IN_PROGRESS, @@ -225,6 +238,7 @@ describe('ProgressComponent', () => { { provide: TestRunService, useValue: testRunServiceMock }, { provide: FocusManagerService, useValue: stateServiceMock }, { provide: LoaderService, useValue: loaderServiceMock }, + { provide: NotificationService, useValue: notificationServiceMock }, { provide: MatDialogRef, useValue: {}, @@ -233,6 +247,8 @@ describe('ProgressComponent', () => { selectors: [ { selector: selectHasDevices, value: false }, { selector: selectDevices, value: [] }, + { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectIsStopTestrun, value: false }, ], }), ], diff --git a/modules/ui/src/app/pages/testrun/progress.component.ts b/modules/ui/src/app/pages/testrun/progress.component.ts index 999509ef2..a7a17d425 100644 --- a/modules/ui/src/app/pages/testrun/progress.component.ts +++ b/modules/ui/src/app/pages/testrun/progress.component.ts @@ -34,6 +34,7 @@ import { FocusManagerService } from '../../services/focus-manager.service'; import { combineLatest } from 'rxjs/internal/observable/combineLatest'; import { TestrunStore } from './testrun.store'; import { TestRunService } from '../../services/test-run.service'; +import { NotificationService } from '../../services/notification.service'; @Component({ selector: 'app-progress', @@ -53,6 +54,7 @@ export class ProgressComponent implements OnInit, OnDestroy { constructor( private readonly testRunService: TestRunService, + private readonly notificationService: NotificationService, public dialog: MatDialog, private readonly focusManagerService: FocusManagerService, public testrunStore: TestrunStore @@ -70,6 +72,15 @@ export class ProgressComponent implements OnInit, OnDestroy { this.openTestRunModal(); } }); + + this.testrunStore.isStopTestrun$ + .pipe(takeUntil(this.destroy$)) + .subscribe(isStop => { + if (isStop) { + this.stopTestrun(); + this.notificationService.dismissSnackBar(); + } + }); } isTestrunInProgress(status?: string) { @@ -125,6 +136,7 @@ export class ProgressComponent implements OnInit, OnDestroy { } ngOnDestroy() { + this.notificationService.dismissSnackBar(); this.destroy$.next(true); this.destroy$.unsubscribe(); this.testrunStore.destroyInterval(); diff --git a/modules/ui/src/app/pages/testrun/testrun.store.spec.ts b/modules/ui/src/app/pages/testrun/testrun.store.spec.ts index 53f173e41..7cd5de718 100644 --- a/modules/ui/src/app/pages/testrun/testrun.store.spec.ts +++ b/modules/ui/src/app/pages/testrun/testrun.store.spec.ts @@ -28,6 +28,8 @@ import { selectHasConnectionSettings, selectHasDevices, selectIsOpenStartTestrun, + selectIsOpenWaitSnackBar, + selectIsStopTestrun, selectIsTestrunStarted, selectSystemStatus, } from '../../store/selectors'; @@ -50,6 +52,7 @@ import { TEST_DATA_TABLE_RESULT, } from '../../mocks/progress.mock'; import { LoaderService } from '../../services/loader.service'; +import { NotificationService } from '../../services/notification.service'; describe('TestrunStore', () => { let testrunStore: TestrunStore; @@ -59,6 +62,11 @@ describe('TestrunStore', () => { 'loaderServiceMock', ['setLoading', 'getLoading'] ); + const notificationServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('NotificationService', [ + 'dismissWithTimout', + 'openSnackBar', + ]); beforeEach(() => { mockService = jasmine.createSpyObj('mockService', [ @@ -71,6 +79,7 @@ describe('TestrunStore', () => { TestrunStore, { provide: TestRunService, useValue: mockService }, { provide: LoaderService, useValue: loaderServiceMock }, + { provide: NotificationService, useValue: notificationServiceMock }, provideMockStore({ selectors: [ { selector: selectHasDevices, value: false }, @@ -78,6 +87,8 @@ describe('TestrunStore', () => { { selector: selectIsTestrunStarted, value: true }, { selector: selectHasConnectionSettings, value: true }, { selector: selectIsOpenStartTestrun, value: false }, + { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectIsStopTestrun, value: false }, ], }), ], diff --git a/modules/ui/src/app/pages/testrun/testrun.store.ts b/modules/ui/src/app/pages/testrun/testrun.store.ts index bad911f93..6b5392355 100644 --- a/modules/ui/src/app/pages/testrun/testrun.store.ts +++ b/modules/ui/src/app/pages/testrun/testrun.store.ts @@ -17,13 +17,15 @@ import { Injectable } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; import { TestRunService } from '../../services/test-run.service'; -import { exhaustMap, interval, Subject } from 'rxjs'; +import { exhaustMap, interval, Subject, take, timer } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; import { AppState } from '../../store/state'; import { Store } from '@ngrx/store'; import { selectHasDevices, selectIsOpenStartTestrun, + selectIsOpenWaitSnackBar, + selectIsStopTestrun, selectIsTestrunStarted, selectSystemStatus, } from '../../store/selectors'; @@ -42,8 +44,10 @@ import { import { takeUntil } from 'rxjs/internal/operators/takeUntil'; import { FocusManagerService } from '../../services/focus-manager.service'; import { LoaderService } from '../../services/loader.service'; +import { NotificationService } from '../../services/notification.service'; const EMPTY_RESULT = new Array(100).fill(null).map(() => ({}) as IResult); +const WAIT_TO_OPEN_SNACKBAR_MS = 60 * 1000; export interface TestrunComponentState { dataSource: IResult[] | undefined; @@ -55,6 +59,7 @@ export interface TestrunComponentState { @Injectable() export class TestrunStore extends ComponentStore { private destroyInterval$: Subject = new Subject(); + private destroyWaitDeviceInterval$: Subject = new Subject(); private dataSource$ = this.select(state => state.dataSource); private isCancelling$ = this.select(state => state.isCancelling); private startInterval$ = this.select(state => state.startInterval); @@ -64,6 +69,8 @@ export class TestrunStore extends ComponentStore { private hasDevices$ = this.store.select(selectHasDevices); private systemStatus$ = this.store.select(selectSystemStatus); isTestrunStarted$ = this.store.select(selectIsTestrunStarted); + isStopTestrun$ = this.store.select(selectIsStopTestrun); + isOpenWaitSnackBar$ = this.store.select(selectIsOpenWaitSnackBar); isOpenStartTestrun$ = this.store.select(selectIsOpenStartTestrun); viewModel$ = this.select({ hasDevices: this.hasDevices$, @@ -102,7 +109,11 @@ export class TestrunStore extends ComponentStore { return trigger$.pipe( exhaustMap(() => { return this.testRunService.fetchSystemStatus().pipe( - withLatestFrom(this.isCancelling$, this.startInterval$), + withLatestFrom( + this.isCancelling$, + this.startInterval$, + this.isOpenWaitSnackBar$ + ), // change status if cancelling in process tap(([res, isCancelling]) => { if (isCancelling && res.status !== StatusOfTestrun.Cancelled) { @@ -110,12 +121,24 @@ export class TestrunStore extends ComponentStore { } }), // perform some additional actions - tap(([res, , startInterval]) => { + tap(([res, , startInterval, isOpenWaitSnackBar]) => { this.store.dispatch(setTestrunStatus({ systemStatus: res })); if (this.testrunInProgress(res.status) && !startInterval) { this.pullingSystemStatusData(); } + if ( + res.status === StatusOfTestrun.WaitingForDevice && + !isOpenWaitSnackBar + ) { + this.showSnackBar(); + } + if ( + res.status !== StatusOfTestrun.WaitingForDevice && + isOpenWaitSnackBar + ) { + this.notificationService.dismissWithTimout(); + } if ( res.status === StatusOfTestrun.WaitingForDevice || res.status === StatusOfTestrun.Monitoring || @@ -232,6 +255,22 @@ export class TestrunStore extends ComponentStore { this.loaderService.setLoading(true); } + private showSnackBar() { + timer(WAIT_TO_OPEN_SNACKBAR_MS) + .pipe( + take(1), + takeUntil(this.destroyWaitDeviceInterval$), + withLatestFrom(this.systemStatus$), + tap(([, systemStatus]) => { + if (systemStatus?.status === StatusOfTestrun.WaitingForDevice) { + this.notificationService.openSnackBar(); + this.destroyWaitDeviceInterval$.next(true); + } + }) + ) + .subscribe(); + } + private pullingSystemStatusData(): void { this.updateStartInterval(true); interval(5000) @@ -262,6 +301,7 @@ export class TestrunStore extends ComponentStore { constructor( private testRunService: TestRunService, + private notificationService: NotificationService, private store: Store, private readonly focusManagerService: FocusManagerService, private readonly loaderService: LoaderService diff --git a/modules/ui/src/app/services/notification.service.spec.ts b/modules/ui/src/app/services/notification.service.spec.ts index 8ba6df88b..d8e6e0ca3 100644 --- a/modules/ui/src/app/services/notification.service.spec.ts +++ b/modules/ui/src/app/services/notification.service.spec.ts @@ -22,20 +22,30 @@ import { TextOnlySnackBar, } from '@angular/material/snack-bar'; import { of } from 'rxjs/internal/observable/of'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { AppState } from '../store/state'; +import { SnackBarComponent } from '../components/snack-bar/snack-bar.component'; describe('NotificationService', () => { let service: NotificationService; + let store: MockStore; const mockMatSnackBar = { open: () => ({}), dismiss: () => ({}), + openFromComponent: () => ({}), }; beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ provide: MatSnackBar, useValue: mockMatSnackBar }], + providers: [ + { provide: MatSnackBar, useValue: mockMatSnackBar }, + provideMockStore({}), + ], }); service = TestBed.inject(NotificationService); + store = TestBed.inject(MockStore); + spyOn(store, 'dispatch').and.callFake(() => {}); }); it('should be created', () => { @@ -76,6 +86,25 @@ describe('NotificationService', () => { }); }); + describe('openSnackBar', () => { + it('should open snackbar fromComponent', () => { + const openSpy = spyOn( + service.snackBar, + 'openFromComponent' + ).and.returnValues({ + afterOpened: () => of(void 0), + afterDismissed: () => of({ dismissedByAction: true }), + } as MatSnackBarRef); + + service.openSnackBar(); + + expect(openSpy).toHaveBeenCalledWith(SnackBarComponent, { + duration: 0, + panelClass: 'snack-bar-info', + }); + }); + }); + describe('dismiss', () => { it('should close snackbar', () => { const matSnackBarSpy = spyOn(mockMatSnackBar, 'dismiss').and.stub(); diff --git a/modules/ui/src/app/services/notification.service.ts b/modules/ui/src/app/services/notification.service.ts index aab437c97..0cc6d4927 100644 --- a/modules/ui/src/app/services/notification.service.ts +++ b/modules/ui/src/app/services/notification.service.ts @@ -23,16 +23,25 @@ import { import { FocusManagerService } from './focus-manager.service'; import { delay } from 'rxjs/internal/operators/delay'; import { take } from 'rxjs/internal/operators/take'; +import { SnackBarComponent } from '../components/snack-bar/snack-bar.component'; +import { timer } from 'rxjs'; +import { setIsOpenWaitSnackBar } from '../store/actions'; +import { Store } from '@ngrx/store'; +import { AppState } from '../store/state'; const TIMEOUT_MS = 8000; +const WAIT_DISMISS_TIMEOUT_MS = 5000; @Injectable({ providedIn: 'root', }) export class NotificationService { private snackBarRef!: MatSnackBarRef; + public snackBarCompRef!: MatSnackBarRef; + constructor( public snackBar: MatSnackBar, + private store: Store, private focusManagerService: FocusManagerService ) {} @@ -57,10 +66,39 @@ export class NotificationService { dismiss() { this.snackBar.dismiss(); } + openSnackBar() { + this.snackBarCompRef = this.snackBar.openFromComponent(SnackBarComponent, { + duration: 0, + panelClass: 'snack-bar-info', + }); + + this.store.dispatch(setIsOpenWaitSnackBar({ isOpenWaitSnackBar: true })); + + this.snackBarCompRef + .afterOpened() + .pipe(take(1), delay(TIMEOUT_MS)) + .subscribe(() => this.setFocusToActionButton()); + + this.snackBarCompRef + .afterDismissed() + .pipe(take(1)) + .subscribe(() => this.focusManagerService.focusFirstElementInContainer()); + } + + dismissSnackBar() { + this.snackBarCompRef?.dismiss(); + this.store.dispatch(setIsOpenWaitSnackBar({ isOpenWaitSnackBar: false })); + } + + dismissWithTimout() { + timer(WAIT_DISMISS_TIMEOUT_MS) + .pipe(take(1)) + .subscribe(() => this.dismissSnackBar()); + } private setFocusToActionButton(): void { const btn = document.querySelector( - '.test-run-notification button' + '.test-run-notification button, .snack-bar-info button' ) as HTMLButtonElement; btn?.focus(); } diff --git a/modules/ui/src/app/store/actions.ts b/modules/ui/src/app/store/actions.ts index 5be2a25b4..4a6081760 100644 --- a/modules/ui/src/app/store/actions.ts +++ b/modules/ui/src/app/store/actions.ts @@ -66,6 +66,16 @@ export const setIsOpenAddDevice = createAction( props<{ isOpenAddDevice: boolean }>() ); +export const setIsStopTestrun = createAction( + '[Shared] Set Is Stop Testrun', + props<{ isStopTestrun: boolean }>() +); + +export const setIsOpenWaitSnackBar = createAction( + '[Shared] Set Is Open WaitSnackBar', + props<{ isOpenWaitSnackBar: boolean }>() +); + export const setHasDevices = createAction( '[Shared] Set Has Devices', props<{ hasDevices: boolean }>() diff --git a/modules/ui/src/app/store/reducers.spec.ts b/modules/ui/src/app/store/reducers.spec.ts index 895ee6b8f..bea3b69f7 100644 --- a/modules/ui/src/app/store/reducers.spec.ts +++ b/modules/ui/src/app/store/reducers.spec.ts @@ -23,6 +23,8 @@ import { setHasDevices, setIsOpenAddDevice, setIsOpenStartTestrun, + setIsOpenWaitSnackBar, + setIsStopTestrun, setIsTestrunStarted, setTestrunStatus, toggleMenu, @@ -131,6 +133,30 @@ describe('Reducer', () => { }); }); + describe('setIsStopTestrun action', () => { + it('should update state', () => { + const initialState = initialSharedState; + const action = setIsStopTestrun({ isStopTestrun: true }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { ...initialState, ...{ isStopTestrun: true } }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('setIsOpenWaitSnackBar action', () => { + it('should update state', () => { + const initialState = initialSharedState; + const action = setIsOpenWaitSnackBar({ isOpenWaitSnackBar: true }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { ...initialState, ...{ isOpenWaitSnackBar: true } }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + describe('setHasDevices action', () => { it('should update state', () => { const initialState = initialSharedState; diff --git a/modules/ui/src/app/store/reducers.ts b/modules/ui/src/app/store/reducers.ts index 17581c735..c7966eb85 100644 --- a/modules/ui/src/app/store/reducers.ts +++ b/modules/ui/src/app/store/reducers.ts @@ -53,6 +53,18 @@ export const sharedReducer = createReducer( isOpenAddDevice, }; }), + on(Actions.setIsStopTestrun, (state, { isStopTestrun }) => { + return { + ...state, + isStopTestrun, + }; + }), + on(Actions.setIsOpenWaitSnackBar, (state, { isOpenWaitSnackBar }) => { + return { + ...state, + isOpenWaitSnackBar, + }; + }), on(Actions.setHasDevices, (state, { hasDevices }) => { return { ...state, diff --git a/modules/ui/src/app/store/selectors.spec.ts b/modules/ui/src/app/store/selectors.spec.ts index 87012dd29..4b5a0f9eb 100644 --- a/modules/ui/src/app/store/selectors.spec.ts +++ b/modules/ui/src/app/store/selectors.spec.ts @@ -24,6 +24,8 @@ import { selectInterfaces, selectIsOpenAddDevice, selectIsOpenStartTestrun, + selectIsOpenWaitSnackBar, + selectIsStopTestrun, selectIsTestrunStarted, selectMenuOpened, selectSystemStatus, @@ -44,6 +46,8 @@ describe('Selectors', () => { devices: [], hasDevices: false, isOpenAddDevice: false, + isStopTestrun: false, + isOpenWaitSnackBar: false, isOpenStartTestrun: false, systemStatus: null, isTestrunStarted: false, @@ -101,6 +105,16 @@ describe('Selectors', () => { expect(result).toEqual(false); }); + it('should select isStopTestrun', () => { + const result = selectIsStopTestrun.projector(initialState); + expect(result).toEqual(false); + }); + + it('should select isOpenWaitSnackBar', () => { + const result = selectIsOpenWaitSnackBar.projector(initialState); + expect(result).toEqual(false); + }); + it('should select deviceInProgress', () => { const result = selectDeviceInProgress.projector(initialState); expect(result).toEqual(null); diff --git a/modules/ui/src/app/store/selectors.ts b/modules/ui/src/app/store/selectors.ts index 7ce8538bb..cac0e572e 100644 --- a/modules/ui/src/app/store/selectors.ts +++ b/modules/ui/src/app/store/selectors.ts @@ -72,6 +72,16 @@ export const selectIsTestrunStarted = createSelector( (state: AppState) => state.shared.isTestrunStarted ); +export const selectIsStopTestrun = createSelector( + selectAppState, + (state: AppState) => state.shared.isStopTestrun +); + +export const selectIsOpenWaitSnackBar = createSelector( + selectAppState, + (state: AppState) => state.shared.isOpenWaitSnackBar +); + export const selectIsOpenStartTestrun = createSelector( selectAppState, (state: AppState) => state.shared.isOpenStartTestrun diff --git a/modules/ui/src/app/store/state.ts b/modules/ui/src/app/store/state.ts index 6da1ee6e6..341ee3720 100644 --- a/modules/ui/src/app/store/state.ts +++ b/modules/ui/src/app/store/state.ts @@ -48,6 +48,8 @@ export interface SharedState { isOpenAddDevice: boolean; // app, testrun isOpenStartTestrun: boolean; + isStopTestrun: boolean; + isOpenWaitSnackBar: boolean; deviceInProgress: Device | null; } @@ -63,6 +65,8 @@ export const initialAppComponentState: AppComponentState = { export const initialSharedState: SharedState = { hasConnectionSettings: null, isOpenAddDevice: false, + isStopTestrun: false, + isOpenWaitSnackBar: false, hasDevices: false, devices: [], deviceInProgress: null, diff --git a/modules/ui/src/styles.scss b/modules/ui/src/styles.scss index c0c058ef6..9cd587535 100644 --- a/modules/ui/src/styles.scss +++ b/modules/ui/src/styles.scss @@ -198,6 +198,10 @@ h2.title { --mdc-outlined-text-field-disabled-label-text-color: rgba(0, 0, 0, 0.58); } +.snack-bar-info.mat-mdc-snack-bar-container .mdc-snackbar__surface { + max-width: 780px; +} + body:has(.initiate-test-run-dialog) app-root app-spinner.connection-settings-spinner, diff --git a/modules/ui/src/theming/colors.scss b/modules/ui/src/theming/colors.scss index a54b7e9ab..7fd232d02 100644 --- a/modules/ui/src/theming/colors.scss +++ b/modules/ui/src/theming/colors.scss @@ -16,6 +16,7 @@ $black: #000000; $white: #ffffff; $primary: #1967d2; +$blue-300: #8ab4f8; $secondary: #5f6368; $accent: #008b00; $warn: #c5221f;