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;