From 8f863c95d2bbddf454cfabfe9be3f2968ba5f1e9 Mon Sep 17 00:00:00 2001 From: Volha Mardvilka Date: Mon, 24 Mar 2025 11:27:48 +0000 Subject: [PATCH 1/4] 403525535: (feat) add help tips --- modules/ui/src/app/app.component.html | 80 +++++++++---- modules/ui/src/app/app.component.scss | 7 +- modules/ui/src/app/app.component.spec.ts | 11 +- modules/ui/src/app/app.component.ts | 26 ++++- .../help-tip/help-tip.component.html | 48 ++++++++ .../help-tip/help-tip.component.scss | 107 ++++++++++++++++++ .../help-tip/help-tip.component.spec.ts | 22 ++++ .../components/help-tip/help-tip.component.ts | 99 ++++++++++++++++ modules/ui/src/app/model/tip-config.ts | 41 +++++++ modules/ui/src/styles.scss | 3 +- 10 files changed, 419 insertions(+), 25 deletions(-) create mode 100644 modules/ui/src/app/components/help-tip/help-tip.component.html create mode 100644 modules/ui/src/app/components/help-tip/help-tip.component.scss create mode 100644 modules/ui/src/app/components/help-tip/help-tip.component.spec.ts create mode 100644 modules/ui/src/app/components/help-tip/help-tip.component.ts create mode 100644 modules/ui/src/app/model/tip-config.ts diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html index c5b5a7e97..6df748145 100644 --- a/modules/ui/src/app/app.component.html +++ b/modules/ui/src/app/app.component.html @@ -84,6 +84,15 @@

Testrun

+
+ + +
+ + + + +
+
diff --git a/modules/ui/src/app/app.component.scss b/modules/ui/src/app/app.component.scss index 0115cf2ae..8d5e82cca 100644 --- a/modules/ui/src/app/app.component.scss +++ b/modules/ui/src/app/app.component.scss @@ -105,7 +105,8 @@ $nav-width: 96px; min-width: 24px; } -.app-sidebar-button-active { +.app-sidebar-button-active, +:host:has(app-help-tip) .app-toolbar-button-help-tips { .material-symbols-outlined { font-variation-settings: 'FILL' 1, @@ -230,3 +231,7 @@ app-version { padding-top: 82px; } } + +.closed-tip { + display: none; +} diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index e1bd105c5..1ae949560 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -332,18 +332,25 @@ describe('AppComponent', () => { }); it('should have version', () => { + fixture.detectChanges(); + component.ngAfterViewInit(); fixture.detectChanges(); const version = compiled.querySelector('app-version'); + fixture.detectChanges(); expect(version).toBeTruthy(); }); - it('should internet icon', () => { + it('should internet icon', fakeAsync(() => { + tick(200); fixture.detectChanges(); + // await fixture.whenStable(); + tick(200); + // await component.ngAfterViewInit(); const internet = compiled.querySelector('app-wifi'); expect(internet).toBeTruthy(); - }); + })); describe('Testing complete', () => { beforeEach(() => { diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index e2f1c82f3..790a5f273 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -20,6 +20,7 @@ import { ElementRef, viewChild, inject, + ViewChild, } from '@angular/core'; import { MatIconModule, MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @@ -46,7 +47,7 @@ import { BypassComponent } from './components/bypass/bypass.component'; import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.component'; import { SpinnerComponent } from './components/spinner/spinner.component'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatButtonModule } from '@angular/material/button'; +import { MatButton, MatButtonModule } from '@angular/material/button'; import { VersionComponent } from './components/version/version.component'; import { MatSelectModule } from '@angular/material/select'; import { WifiComponent } from './components/wifi/wifi.component'; @@ -57,6 +58,8 @@ import { CommonModule } from '@angular/common'; import { SideButtonMenuComponent } from './components/side-button-menu/side-button-menu.component'; import { Observable } from 'rxjs/internal/Observable'; import { of } from 'rxjs/internal/observable/of'; +import { HelpTipComponent } from './components/help-tip/help-tip.component'; +import { HelpTips } from './model/tip-config'; export interface AddMenuItem { icon?: string; @@ -96,6 +99,7 @@ const QUALIFICATION_URL = '/assets/icons/qualification.svg'; BypassComponent, VersionComponent, CalloutComponent, + HelpTipComponent, ShutdownAppComponent, WifiComponent, TestingCompleteComponent, @@ -116,11 +120,16 @@ export class AppComponent implements AfterViewInit { public readonly CalloutType = CalloutType; public readonly StatusOfTestrun = StatusOfTestrun; + public readonly HelpTips = HelpTips; public readonly Routes = Routes; viewModel$ = this.appStore.viewModel$; readonly riskAssessmentLink = viewChild('riskAssessmentLink'); private skipCount = 0; + @ViewChild('settingButton', { static: false }) settingButton!: MatButton; + settingTipTarget!: HTMLElement; + deviceTipTarget!: HTMLElement; + isClosedTip = false; navigateToRuntime = () => { this.route.navigate([Routes.Testing]); @@ -206,6 +215,17 @@ export class AppComponent implements AfterViewInit { } ngAfterViewInit() { + // setTimeout(() => { + // this.settingTipTarget = this.settingButton._elementRef.nativeElement; + // this.deviceTipTarget = document.querySelector( + // '.app-sidebar-button.app-sidebar-button-devices' + // ) as HTMLElement; + // }, 0); + this.settingTipTarget = this.settingButton._elementRef.nativeElement; + this.deviceTipTarget = document.querySelector( + '.app-sidebar-button.app-sidebar-button-devices' + ) as HTMLElement; + this.viewModel$ .pipe( filter(({ isStatusLoaded }) => isStatusLoaded === true), @@ -242,6 +262,10 @@ export class AppComponent implements AfterViewInit { this.appStore.setFocusOnPage(); }); } + + onCLoseTip(isClosed: boolean): void { + this.isClosedTip = isClosed; + } consentShown() { this.appStore.setContent(); } diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.html b/modules/ui/src/app/components/help-tip/help-tip.component.html new file mode 100644 index 000000000..8e2122902 --- /dev/null +++ b/modules/ui/src/app/components/help-tip/help-tip.component.html @@ -0,0 +1,48 @@ + + diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.scss b/modules/ui/src/app/components/help-tip/help-tip.component.scss new file mode 100644 index 000000000..1d5ef4798 --- /dev/null +++ b/modules/ui/src/app/components/help-tip/help-tip.component.scss @@ -0,0 +1,107 @@ +/** + * 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. + */ +@use '@angular/material' as mat; +@use 'm3-theme' as *; +@use 'colors'; +@use 'variables'; + +.tip { + position: absolute; + z-index: 100; + width: 256px; + box-sizing: border-box; +} + +.tip-container { + position: relative; + border-radius: 28px; + background: #0b57d0; + color: colors.$white; + box-shadow: + 0px 4px 8px 3px rgba(0, 0, 0, 0.15), + 0px 1px 3px 0px rgba(0, 0, 0, 0.3); + + p { + margin: 0; + } +} + +.tip-container::before { + content: ''; + position: absolute; + border-radius: 4px; + height: 20px; + width: 20px; + background: #0b57d0; + box-sizing: border-box; + transform: rotate(45deg) translate(-50%); +} + +.tip-container.top::before { + top: 0; + left: 50%; +} + +.tip-container.left::before { + top: 50%; + left: 0; +} + +.heading { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 4px 4px 0 24px; +} + +.title { + font-family: 'Google Sans Text'; + font-size: 16px; + font-weight: 500; + line-height: 24px; +} + +.close-button { + margin-bottom: 4px; + + mat-icon { + color: colors.$white; + } +} + +.tip-content { + padding: 0 24px; + font-family: 'Google Sans Text'; + font-size: 14px; + line-height: 20px; +} + +.tip-action-container { + display: flex; + justify-content: flex-end; + padding: 16px 12px 14px 24px; + + .tip-action-link { + display: inline-block; + padding: 10px 24px; + text-decoration: none; + cursor: pointer; + font-family: 'Google Sans Text'; + font-size: 14px; + font-weight: 500; + line-height: 20px; + } +} diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts b/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts new file mode 100644 index 000000000..2febb818a --- /dev/null +++ b/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HelpTipComponent } from './help-tip.component'; + +describe('HelpTipComponent', () => { + let component: HelpTipComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HelpTipComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(HelpTipComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.ts b/modules/ui/src/app/components/help-tip/help-tip.component.ts new file mode 100644 index 000000000..d12421479 --- /dev/null +++ b/modules/ui/src/app/components/help-tip/help-tip.component.ts @@ -0,0 +1,99 @@ +/** + * 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, + HostListener, + input, + OnInit, + output, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { TipConfig } from '../../model/tip-config'; + +@Component({ + selector: 'app-help-tip', + imports: [CommonModule, MatIconModule, MatButtonModule], + templateUrl: './help-tip.component.html', + styleUrl: './help-tip.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HelpTipComponent implements OnInit { + data = input(); + target = input(); + action = input(); + onAction = output(); + onCLoseTip = output(); + tipPosition = { top: 0, left: 0 }; + + @HostListener('window:resize') + onResize() { + this.updateTipPosition(this.target()); + } + + ngOnInit() { + this.updateTipPosition(this.target()); + } + + private updateTipPosition(target: HTMLElement | undefined | null) { + if (!target) { + return; + } + + const targetRect = target.getBoundingClientRect(); + + const tipWidth = 256; + const arrowOffset = 14; + const topOffset = 82; + + let top = 0; + let left = 0; + + switch (this.data()?.position) { + case 'left': + top = targetRect.top + window.scrollY + targetRect.height / 2; // Center tip vertically + left = targetRect.left + window.scrollX - tipWidth - arrowOffset; + break; + case 'right': + top = + targetRect.top + window.scrollY - topOffset + targetRect.height / 2; // Center tip vertically + left = targetRect.right + window.scrollX; + break; + case 'top': + top = targetRect.top + window.scrollY - arrowOffset; // Position above the button + left = + targetRect.left + + window.scrollX + + targetRect.width / 2 - + tipWidth / 2; // Center horizontally above button + break; + case 'bottom': + top = targetRect.bottom + window.scrollY + arrowOffset; // Position below the button + left = + targetRect.left + + window.scrollX + + targetRect.width / 2 - + tipWidth / 2; // Center horizontally below button + break; + default: + throw new Error('Unsupported tip position!'); + } + + this.tipPosition = { top, left }; + } +} diff --git a/modules/ui/src/app/model/tip-config.ts b/modules/ui/src/app/model/tip-config.ts new file mode 100644 index 000000000..eab024c0d --- /dev/null +++ b/modules/ui/src/app/model/tip-config.ts @@ -0,0 +1,41 @@ +/** + * 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 interface TipConfig { + title: string; + content: string; + action: string; + arrowPosition: 'left' | 'right' | 'top' | 'bottom'; + position: 'left' | 'right' | 'top' | 'bottom'; // Position related to the target +} + +export const HelpTips = { + step1: { + title: 'Step 1:', + content: + 'To get started testing, please select your testing interfaces in system\n' + + 'settings.', + action: 'Go to Settings', + position: 'bottom', + arrowPosition: 'top', + } as TipConfig, + step2: { + title: 'Step 2:', + content: 'Create a device to start your first test attempt.', + action: 'Create Device', + position: 'right', + arrowPosition: 'left', + } as TipConfig, +}; diff --git a/modules/ui/src/styles.scss b/modules/ui/src/styles.scss index 26cfd792e..b81653bdd 100644 --- a/modules/ui/src/styles.scss +++ b/modules/ui/src/styles.scss @@ -206,7 +206,8 @@ body { } } -.app-toolbar-button.app-sidebar-button-active { +.app-toolbar-button.app-sidebar-button-active, +body:has(app-help-tip) .app-toolbar-button-help-tips { .mat-mdc-button-persistent-ripple::before { opacity: 1; background: colors.$light-grey; From 5e1c5e4725f5f052e4a6cd5e8457da3d8b7f59d4 Mon Sep 17 00:00:00 2001 From: kurilova Date: Mon, 24 Mar 2025 12:17:34 +0000 Subject: [PATCH 2/4] Partially fix tests --- modules/ui/src/app/app.component.spec.ts | 14 +++++++------- modules/ui/src/app/app.component.ts | 6 ++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 1ae949560..4cb9e1408 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -76,6 +76,7 @@ import { SpinnerComponent } from './components/spinner/spinner.component'; import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.component'; import { TestingCompleteComponent } from './components/testing-complete/testing-complete.component'; import { VersionComponent } from './components/version/version.component'; +import {device, MOCK_MODULES} from './mocks/device.mock'; const windowMock = { location: { @@ -83,7 +84,7 @@ const windowMock = { }, }; -describe('AppComponent', () => { +fdescribe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture; let compiled: HTMLElement; @@ -201,6 +202,8 @@ describe('AppComponent', () => { }, }); + mockService.fetchDevices.and.returnValue(of([])); + mockService.getTestModules.and.returnValue(of([...MOCK_MODULES])); mockMqttService.getNetworkAdapters.and.returnValue(of(MOCK_ADAPTERS)); store = TestBed.inject(MockStore); fixture = TestBed.createComponent(AppComponent); @@ -341,16 +344,13 @@ describe('AppComponent', () => { expect(version).toBeTruthy(); }); - it('should internet icon', fakeAsync(() => { - tick(200); + it('should internet icon', async () => { fixture.detectChanges(); - // await fixture.whenStable(); - tick(200); - // await component.ngAfterViewInit(); + const internet = compiled.querySelector('app-wifi'); expect(internet).toBeTruthy(); - })); + }); describe('Testing complete', () => { beforeEach(() => { diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 790a5f273..0aecd653c 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -20,7 +20,7 @@ import { ElementRef, viewChild, inject, - ViewChild, + ViewChild, ChangeDetectorRef, } from '@angular/core'; import { MatIconModule, MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @@ -168,7 +168,7 @@ export class AppComponent implements AfterViewInit { }, ]; - constructor() { + constructor(private cdr: ChangeDetectorRef) { this.appStore.getDevices(); this.appStore.getRiskProfiles(); this.appStore.getSystemStatus(); @@ -237,6 +237,8 @@ export class AppComponent implements AfterViewInit { this.skipCount = 1; } }); + + this.cdr.detectChanges(); } get isRiskAssessmentRoute(): boolean { From 7ce15702a27a8ea824e5ffa0658ee15acca6e186 Mon Sep 17 00:00:00 2001 From: Volha Mardvilka Date: Mon, 24 Mar 2025 15:10:02 +0000 Subject: [PATCH 3/4] 403525535: (feat) add unit test --- modules/ui/src/app/app.component.spec.ts | 146 +++++++++--------- modules/ui/src/app/app.component.ts | 12 +- .../help-tip/help-tip.component.scss | 10 +- .../help-tip/help-tip.component.spec.ts | 88 ++++++++++- .../components/help-tip/help-tip.component.ts | 2 +- 5 files changed, 171 insertions(+), 87 deletions(-) diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 4cb9e1408..fbaecc850 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -76,7 +76,7 @@ import { SpinnerComponent } from './components/spinner/spinner.component'; import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.component'; import { TestingCompleteComponent } from './components/testing-complete/testing-complete.component'; import { VersionComponent } from './components/version/version.component'; -import {device, MOCK_MODULES} from './mocks/device.mock'; +import { MOCK_MODULES } from './mocks/device.mock'; const windowMock = { location: { @@ -84,7 +84,7 @@ const windowMock = { }, }; -fdescribe('AppComponent', () => { +describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture; let compiled: HTMLElement; @@ -367,32 +367,92 @@ fdescribe('AppComponent', () => { }); }); - describe('Callout component visibility', () => { + describe('Help tip component visibility', () => { describe('with no connection settings', () => { beforeEach(() => { store.overrideSelector(selectHasConnectionSettings, false); fixture.detectChanges(); }); - it('should have callout component with "Step 1" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); + it('should have help tip component with "Step 1" text', () => { + const helpTip = compiled.querySelector('app-help-tip'); + const helpTipTitle = compiled.querySelector('app-help-tip .title'); + const helpTipContent = helpTipTitle?.innerHTML.trim(); - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 1'); + expect(helpTip).toBeTruthy(); + expect(helpTipContent).toContain('Step 1'); }); - it('should have callout content with "System settings" link ', () => { - const calloutLinkEl = compiled.querySelector( - '.callout-action-link' + it('should have help tip content with "Go to Settings" link ', () => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' ) as HTMLAnchorElement; - const calloutLinkContent = calloutLinkEl.innerHTML.trim(); + const helpTipLinkContent = helpTipLinkEl.innerHTML.trim(); + + expect(helpTipLinkEl).toBeTruthy(); + expect(helpTipLinkContent).toContain('Go to Settings'); + }); + }); + + describe('with no devices set', () => { + beforeEach(() => { + store.overrideSelector(selectHasDevices, false); + fixture.detectChanges(); + }); + + it('should have helpTip component', () => { + const helpTip = compiled.querySelector('app-help-tip'); + + expect(helpTip).toBeTruthy(); + }); + + it('should have help tip component with "Step 2" text', () => { + const helpTipTitle = compiled.querySelector('app-help-tip .title'); + const helpTipTitleContent = helpTipTitle?.innerHTML.trim(); + + expect(helpTipTitleContent).toContain('Step 2'); + }); + + it('should have help tip content with "Create Device" link ', () => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' + ) as HTMLAnchorElement; + const helpTipLinkContent = helpTipLinkEl.innerHTML.trim(); + + expect(helpTipLinkEl).toBeTruthy(); + expect(helpTipLinkContent).toContain('Device'); + }); + + keyboardCases.forEach(testCase => { + it(`should navigate to the device-repository on keydown ${testCase.name} "Create Device" link`, fakeAsync(() => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' + ) as HTMLAnchorElement; + + helpTipLinkEl.dispatchEvent(testCase.event); + flush(); - expect(calloutLinkEl).toBeTruthy(); - expect(calloutLinkContent).toContain('System settings'); + expect(router.url).toBe(Routes.Devices); + })); }); + + it('should navigate to the device-repository on click "Create a Device" link', fakeAsync(() => { + const helpTipLinkEl = compiled.querySelector( + '.tip-action-link' + ) as HTMLAnchorElement; + + helpTipLinkEl.click(); + flush(); + + expect(router.url).toBe(Routes.Devices); + expect(store.dispatch).toHaveBeenCalledWith( + setIsOpenAddDevice({ isOpenAddDevice: true }) + ); + })); }); + }); + describe('Callout component visibility', () => { describe('with system status as "Idle"', () => { beforeEach(() => { component.appStore.updateIsStatusLoaded(true); @@ -486,64 +546,6 @@ fdescribe('AppComponent', () => { }); }); - describe('with no devices setted', () => { - beforeEach(() => { - store.overrideSelector(selectHasDevices, false); - fixture.detectChanges(); - }); - - it('should have callout component', () => { - const callout = compiled.querySelector('app-callout'); - - expect(callout).toBeTruthy(); - }); - - it('should have callout component with "Step 2" text', () => { - const callout = compiled.querySelector('app-callout'); - const calloutContent = callout?.innerHTML.trim(); - - expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Step 2'); - }); - - it('should have callout content with "Create a Device" link ', () => { - const calloutLinkEl = compiled.querySelector( - '.callout-action-link' - ) as HTMLAnchorElement; - const calloutLinkContent = calloutLinkEl.innerHTML.trim(); - - expect(calloutLinkEl).toBeTruthy(); - expect(calloutLinkContent).toContain('Devices'); - }); - - keyboardCases.forEach(testCase => { - it(`should navigate to the device-repository on keydown ${testCase.name} "Create a Device" link`, fakeAsync(() => { - const calloutLinkEl = compiled.querySelector( - '.callout-action-link' - ) as HTMLAnchorElement; - - calloutLinkEl.dispatchEvent(testCase.event); - flush(); - - expect(router.url).toBe(Routes.Devices); - })); - }); - - it('should navigate to the device-repository on click "Create a Device" link', fakeAsync(() => { - const calloutLinkEl = compiled.querySelector( - '.callout-action-link' - ) as HTMLAnchorElement; - - calloutLinkEl.click(); - flush(); - - expect(router.url).toBe(Routes.Devices); - expect(store.dispatch).toHaveBeenCalledWith( - setIsOpenAddDevice({ isOpenAddDevice: true }) - ); - })); - }); - describe('with devices setted but without systemStatus data', () => { beforeEach(() => { store.overrideSelector(selectHasDevices, true); diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 0aecd653c..857959aba 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -20,7 +20,8 @@ import { ElementRef, viewChild, inject, - ViewChild, ChangeDetectorRef, + ViewChild, + ChangeDetectorRef, } from '@angular/core'; import { MatIconModule, MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @@ -116,6 +117,7 @@ export class AppComponent implements AfterViewInit { private store = inject>(Store); private readonly focusManagerService = inject(FocusManagerService); private testRunService = inject(TestRunService); + private cdr = inject(ChangeDetectorRef); appStore = inject(AppStore); public readonly CalloutType = CalloutType; @@ -168,7 +170,7 @@ export class AppComponent implements AfterViewInit { }, ]; - constructor(private cdr: ChangeDetectorRef) { + constructor() { this.appStore.getDevices(); this.appStore.getRiskProfiles(); this.appStore.getSystemStatus(); @@ -215,12 +217,6 @@ export class AppComponent implements AfterViewInit { } ngAfterViewInit() { - // setTimeout(() => { - // this.settingTipTarget = this.settingButton._elementRef.nativeElement; - // this.deviceTipTarget = document.querySelector( - // '.app-sidebar-button.app-sidebar-button-devices' - // ) as HTMLElement; - // }, 0); this.settingTipTarget = this.settingButton._elementRef.nativeElement; this.deviceTipTarget = document.querySelector( '.app-sidebar-button.app-sidebar-button-devices' diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.scss b/modules/ui/src/app/components/help-tip/help-tip.component.scss index 1d5ef4798..2432ef431 100644 --- a/modules/ui/src/app/components/help-tip/help-tip.component.scss +++ b/modules/ui/src/app/components/help-tip/help-tip.component.scss @@ -28,7 +28,7 @@ .tip-container { position: relative; border-radius: 28px; - background: #0b57d0; + background: colors.$primary; color: colors.$white; box-shadow: 0px 4px 8px 3px rgba(0, 0, 0, 0.15), @@ -45,7 +45,7 @@ border-radius: 4px; height: 20px; width: 20px; - background: #0b57d0; + background: colors.$primary; box-sizing: border-box; transform: rotate(45deg) translate(-50%); } @@ -68,7 +68,7 @@ } .title { - font-family: 'Google Sans Text'; + font-family: variables.$font-text; font-size: 16px; font-weight: 500; line-height: 24px; @@ -84,7 +84,7 @@ .tip-content { padding: 0 24px; - font-family: 'Google Sans Text'; + font-family: variables.$font-text; font-size: 14px; line-height: 20px; } @@ -99,7 +99,7 @@ padding: 10px 24px; text-decoration: none; cursor: pointer; - font-family: 'Google Sans Text'; + font-family: variables.$font-text; font-size: 14px; font-weight: 500; line-height: 20px; diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts b/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts index 2febb818a..b3d846ea1 100644 --- a/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts +++ b/modules/ui/src/app/components/help-tip/help-tip.component.spec.ts @@ -1,10 +1,17 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { HelpTipComponent } from './help-tip.component'; +import { HelpTips } from '../../model/tip-config'; describe('HelpTipComponent', () => { let component: HelpTipComponent; let fixture: ComponentFixture; + let compiled: HTMLElement; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -13,10 +20,89 @@ describe('HelpTipComponent', () => { fixture = TestBed.createComponent(HelpTipComponent); component = fixture.componentInstance; + fixture.componentRef.setInput('data', HelpTips.step1); + compiled = fixture.nativeElement as HTMLElement; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have provided data', () => { + const tipTitle = compiled.querySelector('.tip-container .title'); + const tipContent = compiled.querySelector('.tip-container .tip-content'); + + expect(tipTitle?.innerHTML.trim()).toContain(HelpTips.step1.title); + expect(tipContent?.innerHTML.trim()).toContain(HelpTips.step1.content); + }); + + it('should have class provided from arrowPosition', () => { + const tipContainerEl = compiled.querySelector('.tip-container'); + + expect(tipContainerEl?.classList).toContain('top'); + }); + + describe('#updateTipPosition', () => { + beforeEach(() => { + const mockTarget = document.createElement('div'); + spyOn(mockTarget, 'getBoundingClientRect').and.returnValue({ + top: 100, + left: 100, + height: 100, + width: 100, + bottom: 100, + right: 100, + } as DOMRect); + fixture.componentRef.setInput('target', mockTarget); + fixture.detectChanges(); + }); + + it('should update tip position when data.position as "bottom"', () => { + component.ngOnInit(); + + expect(component.tipPosition.left).toBe(22); + expect(component.tipPosition.top).toBe(114); + }); + + it('should update tip position when data.position as "right"', fakeAsync(() => { + fixture.componentRef.setInput('data', HelpTips.step2); + tick(); + + component.ngOnInit(); + + expect(component.tipPosition.left).toBe(100); + expect(component.tipPosition.top).toBe(68); + })); + + it('should update tip position when data.position as "left"', fakeAsync(() => { + const mockData = { ...HelpTips.step2, position: 'left' }; + fixture.componentRef.setInput('data', mockData); + tick(); + + component.ngOnInit(); + + expect(component.tipPosition.left).toBe(-170); + expect(component.tipPosition.top).toBe(150); + })); + + it('should update tip position when data.position as "top"', fakeAsync(() => { + const mockData = { ...HelpTips.step2, position: 'top' }; + fixture.componentRef.setInput('data', mockData); + tick(); + + component.ngOnInit(); + + expect(component.tipPosition.left).toBe(22); + expect(component.tipPosition.top).toBe(86); + })); + + it('should call updateTipPosition on window resize', () => { + spyOn(component, 'updateTipPosition'); + + window.dispatchEvent(new Event('resize')); + + expect(component.updateTipPosition).toHaveBeenCalled(); + }); + }); }); diff --git a/modules/ui/src/app/components/help-tip/help-tip.component.ts b/modules/ui/src/app/components/help-tip/help-tip.component.ts index d12421479..6bfc5a627 100644 --- a/modules/ui/src/app/components/help-tip/help-tip.component.ts +++ b/modules/ui/src/app/components/help-tip/help-tip.component.ts @@ -50,7 +50,7 @@ export class HelpTipComponent implements OnInit { this.updateTipPosition(this.target()); } - private updateTipPosition(target: HTMLElement | undefined | null) { + updateTipPosition(target: HTMLElement | undefined | null) { if (!target) { return; } From 64d90c1bff7d096bc44b4d3eeb6ba0485cc8d8c9 Mon Sep 17 00:00:00 2001 From: Volha Mardvilka Date: Mon, 24 Mar 2025 15:18:57 +0000 Subject: [PATCH 4/4] 403525535: (fix) remove unused data --- modules/ui/src/app/app.component.html | 1 - modules/ui/src/app/app.component.spec.ts | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html index 6df748145..4375cc2ef 100644 --- a/modules/ui/src/app/app.component.html +++ b/modules/ui/src/app/app.component.html @@ -160,7 +160,6 @@

Testrun

{ }); it('should have version', () => { - fixture.detectChanges(); - component.ngAfterViewInit(); fixture.detectChanges(); const version = compiled.querySelector('app-version'); - fixture.detectChanges(); expect(version).toBeTruthy(); }); - it('should internet icon', async () => { + it('should internet icon', () => { fixture.detectChanges(); - const internet = compiled.querySelector('app-wifi'); expect(internet).toBeTruthy();