diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index f69b80548..859cc7d64 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -143,6 +143,7 @@ describe('AppComponent', () => { AppComponent, FakeGeneralSettingsComponent, FakeSpinnerComponent, + FakeShutdownAppComponent, FakeVersionComponent, ], }); @@ -657,6 +658,14 @@ class FakeGeneralSettingsComponent { }) class FakeSpinnerComponent {} +@Component({ + selector: 'app-shutdown-app', + template: '
', +}) +class FakeShutdownAppComponent { + @Input() disable!: boolean; +} + @Component({ selector: 'app-version', template: '
', diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index e1791764b..21605e630 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -178,4 +178,12 @@ export class AppComponent implements OnInit { consentShown() { this.appStore.setContent(); } + + testrunInProgress(status?: string): boolean { + return ( + status === StatusOfTestrun.InProgress || + status === StatusOfTestrun.WaitingForDevice || + status === StatusOfTestrun.Monitoring + ); + } } diff --git a/modules/ui/src/app/app.module.ts b/modules/ui/src/app/app.module.ts index 42ade0ddd..753aaadc4 100644 --- a/modules/ui/src/app/app.module.ts +++ b/modules/ui/src/app/app.module.ts @@ -44,6 +44,8 @@ import { EffectsModule } from '@ngrx/effects'; import { AppEffects } from './store/effects'; import { CdkTrapFocus } from '@angular/cdk/a11y'; import { SettingsDropdownComponent } from './pages/settings/components/settings-dropdown/settings-dropdown.component'; +import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.component'; +import { WindowProvider } from './providers/window.provider'; @NgModule({ declarations: [AppComponent, GeneralSettingsComponent], @@ -71,8 +73,10 @@ import { SettingsDropdownComponent } from './pages/settings/components/settings- EffectsModule.forRoot([AppEffects]), CdkTrapFocus, SettingsDropdownComponent, + ShutdownAppComponent, ], providers: [ + WindowProvider, { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html new file mode 100644 index 000000000..1b36a0841 --- /dev/null +++ b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html @@ -0,0 +1,38 @@ + +{{ data.title }}? + + + + + + diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.scss b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.scss new file mode 100644 index 000000000..85ddce69d --- /dev/null +++ b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.scss @@ -0,0 +1,44 @@ +/** + * 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'; + +:host { + display: grid; + overflow: hidden; + width: 570px; + box-sizing: border-box; + padding: 24px 16px 8px 24px; + gap: 10px; +} + +.modal-title { + color: $grey-900; + font-size: 18px; + line-height: 24px; +} + +.modal-content { + font-family: Roboto, sans-serif; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: $grey-800; +} + +.modal-actions { + padding: 0; + min-height: 30px; +} diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts new file mode 100644 index 000000000..9df2dd8d8 --- /dev/null +++ b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts @@ -0,0 +1,100 @@ +/** + * 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 { ShutdownAppModalComponent } from './shutdown-app-modal.component'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { of } from 'rxjs'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('ShutdownAppModalComponent', () => { + let component: ShutdownAppModalComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ShutdownAppModalComponent, + MatDialogModule, + MatButtonModule, + BrowserAnimationsModule, + ], + providers: [ + { + provide: MatDialogRef, + useValue: { + keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), + close: () => ({}), + }, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ShutdownAppModalComponent); + component = fixture.componentInstance; + component.data = { + title: 'title', + content: 'content', + }; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should has title and content', () => { + const title = compiled.querySelector('.modal-title') as HTMLElement; + const content = compiled.querySelector('.modal-content') as HTMLElement; + + expect(title.innerHTML.trim()).toEqual('title?'); + expect(content.innerHTML.trim()).toEqual('content'); + }); + + it('should close dialog on click "cancel" button', () => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const closeButton = compiled.querySelector( + '.cancel-button' + ) as HTMLButtonElement; + + closeButton.click(); + + expect(closeSpy).toHaveBeenCalledWith(); + + closeSpy.calls.reset(); + }); + + it('should close dialog with true on click "confirm" button', () => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const confirmButton = compiled.querySelector( + '.confirm-button' + ) as HTMLButtonElement; + + confirmButton.click(); + + expect(closeSpy).toHaveBeenCalledWith(true); + + closeSpy.calls.reset(); + }); +}); diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.ts b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.ts new file mode 100644 index 000000000..af42aabe9 --- /dev/null +++ b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.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 { EscapableDialogComponent } from '../escapable-dialog/escapable-dialog.component'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; + +interface DialogData { + title?: string; + content?: string; +} + +@Component({ + selector: 'app-shutdown-app-modal', + templateUrl: './shutdown-app-modal.component.html', + styleUrl: './shutdown-app-modal.component.scss', + standalone: true, + imports: [MatDialogModule, MatButtonModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ShutdownAppModalComponent extends EscapableDialogComponent { + constructor( + public override dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData + ) { + super(dialogRef); + } + + confirm() { + this.dialogRef.close(true); + } + + cancel() { + this.dialogRef.close(); + } +} diff --git a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.html b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.html new file mode 100644 index 000000000..a13bd5118 --- /dev/null +++ b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.html @@ -0,0 +1,23 @@ + + diff --git a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.scss b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.scss new file mode 100644 index 000000000..a424bf842 --- /dev/null +++ b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.scss @@ -0,0 +1,28 @@ +/** + * 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. + */ +:host { + ::ng-deep.mat-mdc-icon-button .mat-mdc-button-persistent-ripple { + border-radius: inherit; + } +} +.shutdown-button { + border-radius: 20px; + padding: 0; + box-sizing: border-box; + height: 34px; + margin: 11px 2px 11px 0; + line-height: 50%; +} diff --git a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.spec.ts b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.spec.ts new file mode 100644 index 000000000..522234f5e --- /dev/null +++ b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.spec.ts @@ -0,0 +1,112 @@ +/** + * 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, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; + +import { ShutdownAppComponent } from './shutdown-app.component'; +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { TestRunService } from '../../services/test-run.service'; +import SpyObj = jasmine.SpyObj; +import { of } from 'rxjs'; +import { ShutdownAppModalComponent } from '../shutdown-app-modal/shutdown-app-modal.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { WINDOW } from '../../providers/window.provider'; + +describe('ShutdownAppComponent', () => { + let component: ShutdownAppComponent; + let compiled: HTMLElement; + let fixture: ComponentFixture; + let mockService: SpyObj; + + const windowMock = { + location: { + reload: jasmine.createSpy('reload'), + }, + }; + + beforeEach(async () => { + mockService = jasmine.createSpyObj(['shutdownTestrun']); + + await TestBed.configureTestingModule({ + imports: [ShutdownAppComponent, MatDialogModule, BrowserAnimationsModule], + providers: [ + { provide: TestRunService, useValue: mockService }, + { + provide: MatDialogRef, + useValue: { + close: () => ({}), + }, + }, + { provide: WINDOW, useValue: windowMock }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ShutdownAppComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('#openShutdownModal should call service shutdownTestrun after close', fakeAsync(() => { + mockService.shutdownTestrun.and.returnValue(of(false)); + spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + tick(); + + component.openShutdownModal(); + + expect(mockService.shutdownTestrun).toHaveBeenCalled(); + })); + + it('#openShutdownModal should reload window after shutdownTestrun', fakeAsync(() => { + mockService.shutdownTestrun.and.returnValue(of(true)); + spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + tick(); + + component.openShutdownModal(); + + expect(windowMock.location.reload).toHaveBeenCalled(); + })); + + it('shutdown button should be enable', () => { + const shutdownButton = compiled.querySelector( + '.shutdown-button' + ) as HTMLButtonElement; + + expect(shutdownButton.disabled).toBeFalse(); + }); + + it('shutdown button should be disable', () => { + component.disable = true; + fixture.detectChanges(); + + const shutdownButton = compiled.querySelector( + '.shutdown-button' + ) as HTMLButtonElement; + + expect(shutdownButton.disabled).toBeTrue(); + }); +}); diff --git a/modules/ui/src/app/components/shutdown-app/shutdown-app.component.ts b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.ts new file mode 100644 index 000000000..99a9b06d0 --- /dev/null +++ b/modules/ui/src/app/components/shutdown-app/shutdown-app.component.ts @@ -0,0 +1,92 @@ +/** + * 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, + Input, + OnDestroy, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatDialog } from '@angular/material/dialog'; +import { ShutdownAppModalComponent } from '../shutdown-app-modal/shutdown-app-modal.component'; +import { Subject, takeUntil } from 'rxjs'; +import { TestRunService } from '../../services/test-run.service'; +import { WINDOW } from '../../providers/window.provider'; + +@Component({ + selector: 'app-shutdown-app', + standalone: true, + imports: [CommonModule, MatButtonModule, MatIcon], + templateUrl: './shutdown-app.component.html', + styleUrl: './shutdown-app.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ShutdownAppComponent implements OnDestroy { + @Input() disable: boolean = false; + + private destroy$: Subject = new Subject(); + constructor( + public dialog: MatDialog, + private testRunService: TestRunService, + @Inject(WINDOW) private window: Window + ) {} + + openShutdownModal() { + const dialogRef = this.dialog.open(ShutdownAppModalComponent, { + ariaLabel: 'Shutdown Testrun', + data: { + title: 'Shutdown Testrun', + content: 'Do you want to stop Testrun?', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: 'shutdown-app-dialog', + }); + + dialogRef + ?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe(shutdownApp => { + if (shutdownApp) { + this.shutdownApp(); + } + }); + } + + private shutdownApp(): void { + this.testRunService + .shutdownTestrun() + .pipe(takeUntil(this.destroy$)) + .subscribe(success => { + if (success) { + this.reloadPage(); + } + }); + } + + private reloadPage(): void { + this.window.location.reload(); + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } +} diff --git a/modules/ui/src/app/providers/window.provider.ts b/modules/ui/src/app/providers/window.provider.ts new file mode 100644 index 000000000..3902e6eb9 --- /dev/null +++ b/modules/ui/src/app/providers/window.provider.ts @@ -0,0 +1,27 @@ +import { FactoryProvider, InjectionToken } from '@angular/core'; + +/** + * 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. + */ +export const WINDOW = new InjectionToken('window'); + +export const WindowProvider: FactoryProvider = { + provide: WINDOW, + useFactory: getWindow, +}; + +export function getWindow(): Window { + return window; +} diff --git a/modules/ui/src/app/services/test-run.service.spec.ts b/modules/ui/src/app/services/test-run.service.spec.ts index 6283b87ac..2068c2b80 100644 --- a/modules/ui/src/app/services/test-run.service.spec.ts +++ b/modules/ui/src/app/services/test-run.service.spec.ts @@ -200,6 +200,19 @@ describe('TestRunService', () => { req.flush({}); }); + it('#shutdownTestrun should have necessary request data', () => { + const apiUrl = 'http://localhost:8000/system/shutdown'; + + service.shutdownTestrun().subscribe(res => { + expect(res).toEqual(true); + }); + + const req = httpTestingController.expectOne(apiUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({}); + req.flush({}); + }); + describe('#startTestRun', () => { it('should have necessary request data', () => { const apiUrl = 'http://localhost:8000/system/start'; diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index f47293b1d..72f578571 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -135,6 +135,12 @@ export class TestRunService { .pipe(map(() => true)); } + shutdownTestrun(): Observable { + return this.http + .post<{ success: string }>(`${API_URL}/system/shutdown`, {}) + .pipe(map(() => true)); + } + getTestModules(): TestModule[] { return this.testModules; } diff --git a/modules/ui/src/theming/colors.scss b/modules/ui/src/theming/colors.scss index db007cf16..a54b7e9ab 100644 --- a/modules/ui/src/theming/colors.scss +++ b/modules/ui/src/theming/colors.scss @@ -23,6 +23,7 @@ $color-background-grey: #f8f9fa; $dark-grey: #444746; $grey-700: #5f6368; $grey-800: #3c4043; +$grey-900: #202124; $light-grey: #bdc1c6; $lighter-grey: #dadce0; $green-50: #e6f4ea; From da167a2998da7e70b4210b45da6dbd4d362d3fd0 Mon Sep 17 00:00:00 2001 From: Volha Mardvilka Date: Wed, 24 Apr 2024 12:16:49 +0000 Subject: [PATCH 2/2] 333725552: (feat) ability to shutdown testrun --- .../shutdown-app-modal/shutdown-app-modal.component.html | 2 +- .../shutdown-app-modal/shutdown-app-modal.component.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html index 1b36a0841..0614dfc28 100644 --- a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html +++ b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.html @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -{{ data.title }}? +{{ data.title }} diff --git a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts index 9df2dd8d8..d7cd5abb6 100644 --- a/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts +++ b/modules/ui/src/app/components/shutdown-app-modal/shutdown-app-modal.component.spec.ts @@ -68,7 +68,7 @@ describe('ShutdownAppModalComponent', () => { const title = compiled.querySelector('.modal-title') as HTMLElement; const content = compiled.querySelector('.modal-content') as HTMLElement; - expect(title.innerHTML.trim()).toEqual('title?'); + expect(title.innerHTML.trim()).toEqual('title'); expect(content.innerHTML.trim()).toEqual('content'); });