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';