From fafe3e4de61e254c8d0ac44e2c9351bbb91f8121 Mon Sep 17 00:00:00 2001 From: cv5ch <176032962+cv5ch@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:32:24 +0200 Subject: [PATCH] Add unittests for HomeComponent --- src/app/core/_decorators/autotitle.ts | 26 +- src/app/home/home.component.spec.ts | 336 ++++++++++++++++++++++++++ src/polyfills.ts | 4 +- 3 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 src/app/home/home.component.spec.ts diff --git a/src/app/core/_decorators/autotitle.ts b/src/app/core/_decorators/autotitle.ts index 2359ef6fa..fd3179810 100644 --- a/src/app/core/_decorators/autotitle.ts +++ b/src/app/core/_decorators/autotitle.ts @@ -1,17 +1,21 @@ -import { AutoTitleService } from "../_services/shared/autotitle.service"; -import { AppModule } from "src/app/app.module"; +import { AutoTitleService } from '@services/shared/autotitle.service'; -export function PageTitle(title: string | string[]): ClassDecorator { - - return function (constructor: any){ - const titleService = AppModule.injector.get(AutoTitleService); +import { AppModule } from '@src/app/app.module'; - const ngOnInit = constructor.prototype.ngOnInit; +export function PageTitle(title: string | string[]): ClassDecorator { + return function (constructor: any) { + const originalNgOnInit = constructor.prototype.ngOnInit; constructor.prototype.ngOnInit = function () { - ngOnInit && ngOnInit.apply(this); - titleService.set(title) - } - } + if (!this.__titleService) { + // Lazy get the service only once per instance + this.__titleService = AppModule.injector.get(AutoTitleService); + } + if (originalNgOnInit) { + originalNgOnInit.apply(this); + } + this.__titleService.set(title); + }; + }; } diff --git a/src/app/home/home.component.spec.ts b/src/app/home/home.component.spec.ts new file mode 100644 index 000000000..d6809f092 --- /dev/null +++ b/src/app/home/home.component.spec.ts @@ -0,0 +1,336 @@ +import { of } from 'rxjs'; + +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { Component, DebugElement, Injector, Input } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { By } from '@angular/platform-browser'; +import { RouterLink, RouterLinkWithHref, provideRouter } from '@angular/router'; + +import { Filter, FilterType, RequestParams } from '@models/request-params.model'; +import { TaskType } from '@models/task.model'; + +import { SERV, ServiceConfig } from '@services/main.config'; +import { GlobalService } from '@services/main.service'; +import { PermissionService } from '@services/permission/permission.service'; +import { LocalStorageService } from '@services/storage/local-storage.service'; + +import { AppModule } from '@src/app/app.module'; +import { Perm, PermissionValues } from '@src/app/core/_constants/userpermissions.config'; +import { PageTitle } from '@src/app/core/_decorators/autotitle'; +import { HomeComponent } from '@src/app/home/home.component'; + +/** + * Stub component to replace the real app-heatmap-chart component in tests. + * Prevents the need to load its internal logic or dependencies. + */ +@Component({ + selector: 'app-heatmap-chart', + template: '
Heatmap Chart Stub
' +}) +class HeatmapChartStubComponent { + /** Input for heatmap data */ + @Input() data: unknown; + + /** Input for dark mode styling */ + @Input() isDarkMode: boolean; +} + +/** + * Dummy component used for routing configuration during tests. + * Acts as a placeholder target for router navigation. + */ +@Component({ template: '' }) +class DummyComponent {} + +/** + * Stub routes used during test setup to initialize the RouterTestingModule. + */ +const routes = [{ path: '', component: DummyComponent }]; + +/** + * Mock implementation of GlobalService. + * Simulates various backend service responses based on service type and filter params. + */ +const globalServiceMock = jasmine.createSpyObj('GlobalService', ['getAll']); + +/** + * Conditional return logic for the mocked `getAll` method of GlobalService. + * Returns different counts depending on the service and filter parameters. + */ +globalServiceMock.getAll.and.callFake((service: ServiceConfig, params?: RequestParams) => { + // Handle TASKS_WRAPPER_COUNT — differentiate between Supertasks and regular Tasks + if (service === SERV.TASKS_WRAPPER_COUNT) { + const isSuperTask = params?.filter?.some( + (filter: Filter) => filter.field === 'taskType' && filter.value === TaskType.SUPERTASK + ); + + const isCompleted = params?.filter?.some( + (filter: Filter) => filter.field === 'keyspace' && filter.operator === FilterType.GREATER + ); + + // Return counts based on task type and completion status + if (isSuperTask) { + return of({ meta: { count: isCompleted ? 5 : 10 } }); // 5 completed, 10 total supertasks + } else { + return of({ meta: { count: isCompleted ? 15 : 30 } }); // 15 completed, 30 total tasks + } + } + + // Handle AGENTS_COUNT — returns total and active agent counts + if (service === SERV.AGENTS_COUNT) { + return of({ meta: { total_count: 50, count: 20 } }); // 20 active, 50 total agents + } + + // Handle HASHES_COUNT — returns number of cracked hashes + if (service === SERV.HASHES_COUNT) { + return of({ meta: { count: 7 } }); // 7 hashes cracked + } + + // Handle HASHES — return an empty dataset for now + if (service === SERV.HASHES) { + return of({ data: [], included: [] }); // simulate empty response + } + + // Default fallback for any other service + return of({ meta: { count: 0 }, results: [] }); +}); + +/** + * Mock implementation of LocalStorageService for testing purposes. + * Simulates localStorage behavior by stubbing `getItem` and `setItem`. + */ +const mockLocalStorageService = { + /** Simulates retrieving a value from localStorage. Returns `null` by default. */ + getItem: jasmine.createSpy('getItem').and.returnValue(null), + + /** Simulates saving a value in localStorage. Does not persist anything. */ + setItem: jasmine.createSpy('setItem') +}; + +/** + * Stub for PageTitleService used to mock the `set` method that updates the page title. + */ +const pageTitleStub = jasmine.createSpyObj('PageTitleService', ['set']); + +/** + * Mock for the PermissionService. + * All permissions return `true` by default unless explicitly overridden. + */ +const permissionServiceMock = jasmine.createSpyObj('PermissionService', ['hasPermissionSync']); +permissionServiceMock.hasPermissionSync.and.returnValue(true); + +describe('HomeComponent (template permissions and view)', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + let debugEl: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [HomeComponent], + imports: [MatCardModule, MatIconModule, RouterLink, RouterLinkWithHref, HeatmapChartStubComponent], + providers: [ + provideHttpClientTesting(), + provideRouter(routes), + { provide: GlobalService, useValue: globalServiceMock }, + { provide: LocalStorageService, useValue: mockLocalStorageService }, + { provide: PageTitle, useValue: pageTitleStub }, + { provide: PermissionService, useValue: permissionServiceMock } + ] + }).compileComponents(); + + AppModule.injector = TestBed.inject(Injector); + + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + + component.activeAgents = 10; + component.totalAgents = 20; + component.completedTasks = 15; + component.totalTasks = 30; + component.completedSupertasks = 5; + component.totalSupertasks = 10; + component.totalCracks = 7; + component.heatmapData = [ + ['2025-07-01', 3], + ['2025-07-02', 4] + ]; + component.lastUpdated = 'July 20, 2025 10:00 AM'; + component.isDarkMode = false; + + permissionServiceMock.hasPermissionSync.calls.reset(); + globalServiceMock.getAll.calls.reset(); + + fixture.detectChanges(); + component.setAutoreload(false); + }); + + it('should show agent count when permission is granted', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm === Perm.Agent.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const text = fixture.nativeElement.textContent; + const agentCard = debugEl.query(By.css('.co-25:nth-child(1)')); + expect(text).toContain('20 / 50'); + expect(agentCard.nativeElement.textContent).not.toContain('No permission'); + }); + + it('should show "No permission" for agents when canReadAgents false', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Agent.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const agentCard = debugEl.query(By.css('.co-25:nth-child(1)')); + expect(agentCard.nativeElement.textContent).toContain('No permission'); + expect(agentCard.nativeElement.textContent).not.toContain('10 / 20'); + }); + + it('should show tasks stats and hide "no permission" when canReadTasks true', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm === Perm.Task.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const tasksCard = debugEl.queryAll(By.css('.co-25'))[1]; + expect(tasksCard.nativeElement.textContent).toContain('Tasks'); + expect(tasksCard.nativeElement.textContent).toContain('15 / 30'); + expect(tasksCard.query(By.css('.no-permission'))).toBeNull(); + }); + + it('should show "No permission" for tasks when canReadTasks false', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Task.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const tasksCard = debugEl.queryAll(By.css('.co-25'))[1]; + expect(tasksCard.nativeElement.textContent).toContain('No permission'); + expect(tasksCard.nativeElement.textContent).not.toContain('15 / 30'); + }); + + it('should show supertask count when permission is granted', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm === Perm.Task.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + const text = fixture.nativeElement.textContent; + const noPermText = fixture.debugElement.query(By.css('.supertasks-section .no-permission')); + expect(text).toContain('5 / 10'); + expect(noPermText).toBeNull(); + }); + + it('should show "No permission" for supertasks when canReadTasks false', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Task.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const supertasksCard = debugEl.queryAll(By.css('.co-25'))[2]; + expect(supertasksCard.nativeElement.textContent).toContain('No permission'); + expect(supertasksCard.nativeElement.textContent).not.toContain('5 / 10'); + }); + + it('should show cracks count and hide "no permission" when canReadCracks true', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm === Perm.Hash.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const cracksCard = debugEl.queryAll(By.css('.co-25'))[3]; + expect(cracksCard.nativeElement.textContent).toContain('Cracks'); + expect(cracksCard.nativeElement.textContent).toContain('7'); + expect(cracksCard.query(By.css('.no-permission'))).toBeNull(); + }); + + it('should show "No permission" for cracks when canReadCracks is false', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Hash.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const cracksCard = debugEl.query(By.css('.co-25:nth-child(4)')); + const cracksValueSpan = cracksCard.query(By.css('.value')); + const noPermissionSpan = cracksCard.query(By.css('.no-permission')); + expect(cracksCard).toBeTruthy(); + expect(cracksValueSpan).toBeNull(); + expect(noPermissionSpan).toBeTruthy(); + expect(noPermissionSpan.nativeElement.textContent).toContain('No permission'); + }); + + it('should NOT show heatmap chart when canReadCracks false', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Hash.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const heatmap = debugEl.query(By.directive(HeatmapChartStubComponent)); + const chartContainer = debugEl.query(By.css('.app-echarts')); + const noPermText = chartContainer.query(By.css('.no-permission')); + expect(heatmap).toBeNull(); + expect(chartContainer).toBeTruthy(); + expect(noPermText).toBeTruthy(); + expect(noPermText.nativeElement.textContent).toContain('No permission to view chart data'); + }); + + it('should show "No permission to view chart data" when no permission for cracks', () => { + permissionServiceMock.hasPermissionSync.and.callFake((perm: PermissionValues) => perm !== Perm.Hash.READ); + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + debugEl = fixture.debugElement; + fixture.detectChanges(); + + const chartContainer = debugEl.query(By.css('.app-echarts')); + const noPermText = chartContainer.query(By.css('.no-permission')); + expect(noPermText.nativeElement.textContent).toContain('No permission to view chart data'); + }); + + it('should show enable auto reload button when refreshPage false', () => { + component.setAutoreload(false); + fixture.detectChanges(); + + const enableButton = debugEl.query(By.css('button[mattooltip="Enable Auto Reload"]')); + expect(enableButton).toBeTruthy(); + }); + + it('should show pause auto reload button when refreshPage true', () => { + component.setAutoreload(true); + fixture.detectChanges(); + + const pauseButton = debugEl.query(By.css('button[mattooltip="Pause Auto Reload"]')); + expect(pauseButton).toBeTruthy(); + }); + + it('should call setAutoreload(true) when enable button clicked', () => { + component.setAutoreload(false); + spyOn(component, 'setAutoreload'); + fixture.detectChanges(); + + const enableButton = debugEl.query(By.css('button[mattooltip="Enable Auto Reload"]')); + enableButton.nativeElement.click(); + expect(component.setAutoreload).toHaveBeenCalledWith(true); + }); + + it('should call setAutoreload(false) when pause button clicked', () => { + component.setAutoreload(true); + spyOn(component, 'setAutoreload'); + fixture.detectChanges(); + + const pauseButton = debugEl.query(By.css('button[mattooltip="Pause Auto Reload"]')); + pauseButton.nativeElement.click(); + expect(component.setAutoreload).toHaveBeenCalledWith(false); + }); +}); diff --git a/src/polyfills.ts b/src/polyfills.ts index 9194e82b3..9c8f6161f 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -49,9 +49,9 @@ import '@angular/localize/init'; /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js'; // Included with Angular CLI. - +import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ +import 'reflect-metadata';