From c87596fea178b47f51b2ea4bbe8fa72a6ff9e549 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Tue, 21 Apr 2026 12:39:33 +0200 Subject: [PATCH] Strengthen frontend spec coverage --- src/app/app.spec.ts | 4 +- src/app/layouts/app-layout/app-layout.spec.ts | 33 +++++++- .../tasks-executor/tasks-executor.spec.ts | 81 ++++++++++++++++++- src/app/pages/auth/login/login.spec.ts | 17 +++- src/app/pages/auth/signup/signup.spec.ts | 16 +++- .../notifications/notification.spec.ts | 14 +++- .../shared/blocks-list/blocks-list.spec.ts | 19 ++++- .../flows-list/flow-item/flow-item.spec.ts | 52 +++++++++++- src/app/shared/flows-list/flows-list.spec.ts | 18 ++++- .../nodes/generic-node/generic-node.spec.ts | 57 ++++++++++++- src/app/shared/ordering/ordering.spec.ts | 3 + .../shared/rete-editor/rete-editor.spec.ts | 45 ++++++++++- src/app/stores/flow-editor.spec.ts | 24 +----- 13 files changed, 349 insertions(+), 34 deletions(-) diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index 9c77149..0987d8f 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -16,8 +16,10 @@ describe('App', () => { it('should render title', async () => { const fixture = TestBed.createComponent(App); + fixture.detectChanges(); await fixture.whenStable(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, humainFlow-gui-a21'); + expect(compiled.querySelector('router-outlet')).toBeTruthy(); + expect(compiled.querySelector('app-global-notification')).toBeTruthy(); }); }); diff --git a/src/app/layouts/app-layout/app-layout.spec.ts b/src/app/layouts/app-layout/app-layout.spec.ts index 93ba684..9b70eb5 100644 --- a/src/app/layouts/app-layout/app-layout.spec.ts +++ b/src/app/layouts/app-layout/app-layout.spec.ts @@ -1,4 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { signal } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { Authorization } from '@services/authorization/authorization'; +import { BlocksService } from '@services/blocks/blocks'; +import { ContainersService } from '@services/containers/containers'; +import { of } from 'rxjs'; +import { vi } from 'vitest'; import { AppLayout } from './app-layout'; @@ -8,12 +15,36 @@ describe('AppLayout', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AppLayout] + imports: [AppLayout], + providers: [ + provideRouter([]), + { + provide: Authorization, + useValue: { + loggedInUser: signal(null), + logout: vi.fn().mockReturnValue(of(null)), + changePassword: vi.fn().mockReturnValue(of(null)) + } + }, + { + provide: BlocksService, + useValue: { + getAllBlocksTypes: vi.fn().mockResolvedValue(signal([])) + } + }, + { + provide: ContainersService, + useValue: { + getAllContainerTypes: vi.fn().mockResolvedValue(signal([])) + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(AppLayout); component = fixture.componentInstance; + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/layouts/tasks-executor/tasks-executor.spec.ts b/src/app/layouts/tasks-executor/tasks-executor.spec.ts index 102831f..17605ae 100644 --- a/src/app/layouts/tasks-executor/tasks-executor.spec.ts +++ b/src/app/layouts/tasks-executor/tasks-executor.spec.ts @@ -1,4 +1,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { signal } from '@angular/core'; +import { ActivatedRoute, convertToParamMap, provideRouter, Router } from '@angular/router'; +import { BlocksService } from '@services/blocks/blocks'; +import { ContainersService } from '@services/containers/containers'; +import { HumanInteractionDialogService } from '@services/dialogs/human-interaction-dialog'; +import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; +import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog'; +import { FieldRetriever } from '@services/retriever/field-retriever'; +import { TaskExecutionsService } from '@services/task-executions/task-executions'; +import { of } from 'rxjs'; +import { vi } from 'vitest'; import { TasksExecutor } from './tasks-executor'; @@ -8,12 +19,80 @@ describe('TasksExecutor', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TasksExecutor] + imports: [TasksExecutor], + providers: [ + provideRouter([]), + { + provide: ActivatedRoute, + useValue: { + queryParamMap: of(convertToParamMap({})), + snapshot: { + queryParamMap: convertToParamMap({}) + } + } + }, + { + provide: Router, + useValue: { + navigate: vi.fn().mockResolvedValue(true) + } + }, + { + provide: TaskExecutionsService, + useValue: { + taskExecutions: signal([]), + pendingExecutionCreation: signal(false), + init: vi.fn(), + deleteExecution: vi.fn().mockReturnValue(of(null)) + } + }, + { + provide: ConfirmDialogService, + useValue: { + open: vi.fn().mockResolvedValue(true) + } + }, + { + provide: BlocksService, + useValue: { + getAllBlocksTypes: vi.fn().mockResolvedValue(signal([])) + } + }, + { + provide: ContainersService, + useValue: { + getAllContainerTypes: vi.fn().mockResolvedValue(signal([])) + } + }, + { + provide: HumanInteractionDialogService, + useValue: { + state: signal(null), + close: vi.fn(), + update: vi.fn() + } + }, + { + provide: NodeSettingsDialogService, + useValue: { + open: vi.fn().mockResolvedValue(null) + } + }, + { + provide: FieldRetriever, + useValue: { + retrieveText: vi.fn(), + retrieveSchema: vi.fn(), + retrieveStructuredData: vi.fn() + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(TasksExecutor); component = fixture.componentInstance; + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/pages/auth/login/login.spec.ts b/src/app/pages/auth/login/login.spec.ts index 8d4c63e..a0fbd97 100644 --- a/src/app/pages/auth/login/login.spec.ts +++ b/src/app/pages/auth/login/login.spec.ts @@ -1,4 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { Authorization } from '@services/authorization/authorization'; +import { of } from 'rxjs'; +import { vi } from 'vitest'; import { Login } from './login'; @@ -8,12 +12,23 @@ describe('Login', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Login] + imports: [Login], + providers: [ + provideRouter([]), + { + provide: Authorization, + useValue: { + validateSession: vi.fn().mockReturnValue(of(null)), + login: vi.fn().mockReturnValue(of(null)) + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(Login); component = fixture.componentInstance; + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/pages/auth/signup/signup.spec.ts b/src/app/pages/auth/signup/signup.spec.ts index 1e81683..55d8d33 100644 --- a/src/app/pages/auth/signup/signup.spec.ts +++ b/src/app/pages/auth/signup/signup.spec.ts @@ -1,4 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { Authorization } from '@services/authorization/authorization'; +import { of } from 'rxjs'; +import { vi } from 'vitest'; import { Signup } from './signup'; @@ -8,12 +12,22 @@ describe('Signup', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Signup] + imports: [Signup], + providers: [ + provideRouter([]), + { + provide: Authorization, + useValue: { + signup: vi.fn().mockReturnValue(of(null)) + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(Signup); component = fixture.componentInstance; + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/services/notifications/notification.spec.ts b/src/app/services/notifications/notification.spec.ts index ebe21dd..71aab59 100644 --- a/src/app/services/notifications/notification.spec.ts +++ b/src/app/services/notifications/notification.spec.ts @@ -1,9 +1,14 @@ -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; +import { afterEach, vi } from 'vitest'; import { NotificationService } from './notification'; describe('NotificationService', () => { let service: NotificationService; + afterEach(() => { + vi.useRealTimers(); + }); + beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(NotificationService); @@ -24,12 +29,13 @@ describe('NotificationService', () => { expect(service.current()!.type).toBe('success'); }); - it('should auto-dismiss after duration', fakeAsync(() => { + it('should auto-dismiss after duration', async () => { + vi.useFakeTimers(); service.show('Auto dismiss', 'info', 2000); expect(service.current()).toBeTruthy(); - tick(2000); + await vi.advanceTimersByTimeAsync(2000); expect(service.current()).toBeNull(); - })); + }); it('should dismiss manually', () => { service.show('Manual dismiss', 'error', 0); diff --git a/src/app/shared/blocks-list/blocks-list.spec.ts b/src/app/shared/blocks-list/blocks-list.spec.ts index 81c73dd..dffcad1 100644 --- a/src/app/shared/blocks-list/blocks-list.spec.ts +++ b/src/app/shared/blocks-list/blocks-list.spec.ts @@ -1,4 +1,8 @@ +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BlocksService } from '@services/blocks/blocks'; +import { ListState } from '@stores/list-state'; +import { vi } from 'vitest'; import { BlocksList } from './blocks-list'; @@ -8,12 +12,25 @@ describe('BlocksList', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [BlocksList] + imports: [BlocksList], + providers: [ + ListState, + { + provide: BlocksService, + useValue: { + catalogLoading: signal(false), + blockTypes: signal([]), + hasLoadedBlockTypes: vi.fn().mockReturnValue(true), + getAllBlocksTypes: vi.fn().mockResolvedValue(signal([])) + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(BlocksList); component = fixture.componentInstance; + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/shared/flows-list/flow-item/flow-item.spec.ts b/src/app/shared/flows-list/flow-item/flow-item.spec.ts index cb4c1bc..2572a00 100644 --- a/src/app/shared/flows-list/flow-item/flow-item.spec.ts +++ b/src/app/shared/flows-list/flow-item/flow-item.spec.ts @@ -1,4 +1,12 @@ +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { Authorization } from '@services/authorization/authorization'; +import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; +import { FlowsService } from '@services/flows/flows'; +import { EditorStateHolder } from '@stores/flow-editor'; +import { of } from 'rxjs'; +import { vi } from 'vitest'; import { FlowItem } from './flow-item'; @@ -8,12 +16,54 @@ describe('FlowItem', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FlowItem] + imports: [FlowItem], + providers: [ + provideRouter([]), + { + provide: Authorization, + useValue: { + loggedInUser: vi.fn().mockReturnValue({ username: 'author' }) + } + }, + { + provide: ConfirmDialogService, + useValue: { + open: vi.fn().mockResolvedValue(true) + } + }, + { + provide: FlowsService, + useValue: { + cloneFlow: vi.fn().mockReturnValue(of(null)), + deleteFlow: vi.fn().mockReturnValue(of(null)) + } + }, + { + provide: EditorStateHolder, + useValue: { + currentFlow: signal(null), + isDirty: vi.fn().mockReturnValue(false), + openDocument: vi.fn(), + closeDocument: vi.fn() + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(FlowItem); component = fixture.componentInstance; + fixture.componentRef.setInput('flow', { + id: 'flow-1', + name: 'Test flow', + visibility: 'PRIVATE', + data: { blocks: [], containers: [], connections: [], dependencies: [] }, + author: 'author', + createdAt: new Date(), + status: 'DRAFT', + updatedAt: new Date() + }); + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/shared/flows-list/flows-list.spec.ts b/src/app/shared/flows-list/flows-list.spec.ts index 1470b75..6dbe5da 100644 --- a/src/app/shared/flows-list/flows-list.spec.ts +++ b/src/app/shared/flows-list/flows-list.spec.ts @@ -1,4 +1,8 @@ +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FlowsService } from '@services/flows/flows'; +import { ListState } from '@stores/list-state'; +import { vi } from 'vitest'; import { FlowsList } from './flows-list'; @@ -8,12 +12,24 @@ describe('FlowsList', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FlowsList] + imports: [FlowsList], + providers: [ + ListState, + { + provide: FlowsService, + useValue: { + flows: signal([]), + hasLoadedFlows: vi.fn().mockReturnValue(true), + getAllFlows: vi.fn().mockResolvedValue(signal([])) + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(FlowsList); component = fixture.componentInstance; + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/shared/nodes/generic-node/generic-node.spec.ts b/src/app/shared/nodes/generic-node/generic-node.spec.ts index 88ab141..78147b7 100644 --- a/src/app/shared/nodes/generic-node/generic-node.spec.ts +++ b/src/app/shared/nodes/generic-node/generic-node.spec.ts @@ -1,4 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BlocksService } from '@services/blocks/blocks'; +import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog'; +import { FieldRetriever } from '@services/retriever/field-retriever'; +import { EditorStateHolder } from '@stores/flow-editor'; +import { vi } from 'vitest'; import { GenericNodeComponent } from './generic-node'; @@ -8,12 +13,62 @@ describe('GenericNodeComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [GenericNodeComponent] + imports: [GenericNodeComponent], + providers: [ + { + provide: NodeSettingsDialogService, + useValue: { + open: vi.fn().mockResolvedValue(null) + } + }, + { + provide: EditorStateHolder, + useValue: { + currentFlow: vi.fn().mockReturnValue(null), + isBlockSelected: vi.fn().mockReturnValue(false), + isValidationNodeHighlighted: vi.fn().mockReturnValue(false), + updateData: vi.fn(), + stopDraggingSelectedBlocks: vi.fn() + } + }, + { + provide: FieldRetriever, + useValue: { + retrieveSchema: vi.fn(), + retrieveStructuredData: vi.fn(), + retrieveText: vi.fn() + } + }, + { + provide: BlocksService, + useValue: { + peekBlockType: vi.fn().mockReturnValue(null), + getBlockType: vi.fn().mockResolvedValue(null) + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(GenericNodeComponent); component = fixture.componentInstance; + fixture.componentRef.setInput('data', { + id: 'node-1', + inputs: {}, + outputs: {}, + selected: false, + data: { + id: 'node-1', + typeName: '', + name: 'Node 1', + inputs: [], + outputs: [], + specificConfiguration: { name: 'Node 1' } + } + }); + fixture.componentRef.setInput('emit', vi.fn()); + fixture.componentRef.setInput('rendered', vi.fn()); + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/shared/ordering/ordering.spec.ts b/src/app/shared/ordering/ordering.spec.ts index 131d9a8..89d6ed3 100644 --- a/src/app/shared/ordering/ordering.spec.ts +++ b/src/app/shared/ordering/ordering.spec.ts @@ -14,6 +14,9 @@ describe('Ordering', () => { fixture = TestBed.createComponent(Ordering); component = fixture.componentInstance; + fixture.componentRef.setInput('orderView', { orderBy: null, orderDir: 'asc' }); + fixture.componentRef.setInput('orderFields', []); + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/shared/rete-editor/rete-editor.spec.ts b/src/app/shared/rete-editor/rete-editor.spec.ts index 46e1440..e6e8899 100644 --- a/src/app/shared/rete-editor/rete-editor.spec.ts +++ b/src/app/shared/rete-editor/rete-editor.spec.ts @@ -1,4 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BlocksService } from '@services/blocks/blocks'; +import { ContainersService } from '@services/containers/containers'; +import { GraphSelectionService } from '@services/graph-selection/graph-selection'; +import { EditorStateHolder } from '@stores/flow-editor'; +import { vi } from 'vitest'; import { ReteEditor } from './rete-editor'; @@ -8,12 +13,50 @@ describe('ReteEditor', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ReteEditor] + imports: [ReteEditor], + providers: [ + { + provide: EditorStateHolder, + useValue: { + selectedBlockIds: vi.fn().mockReturnValue([]), + stopDraggingSelectedBlocks: vi.fn(), + updateData: vi.fn(), + clearBlockSelection: vi.fn(), + setSelectedBlocks: vi.fn() + } + }, + { + provide: BlocksService, + useValue: {} + }, + { + provide: ContainersService, + useValue: {} + }, + { + provide: GraphSelectionService, + useValue: { + deleteConnectionRequestTick: vi.fn(), + selectedConnectionId: vi.fn().mockReturnValue(null), + clearConnectionSelection: vi.fn() + } + } + ] }) .compileComponents(); fixture = TestBed.createComponent(ReteEditor); component = fixture.componentInstance; + fixture.componentRef.setInput('flowId', 'flow-1'); + fixture.componentRef.setInput('flowData', { + blocks: [], + containers: [], + connections: [], + dependencies: [], + globalInputs: [] + }); + (component as any).reloadEditor = vi.fn().mockResolvedValue(undefined); + fixture.detectChanges(); await fixture.whenStable(); }); diff --git a/src/app/stores/flow-editor.spec.ts b/src/app/stores/flow-editor.spec.ts index ca2c25c..775a285 100644 --- a/src/app/stores/flow-editor.spec.ts +++ b/src/app/stores/flow-editor.spec.ts @@ -1,8 +1,8 @@ import { TestBed } from '@angular/core/testing'; import { Authorization } from '@services/authorization/authorization'; import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; -import { FlowsService } from '@services/flows/flows'; import { Flow, FlowData } from '@models/flow'; +import { of } from 'rxjs'; import { vi } from 'vitest'; import { EditorStateHolder } from './flow-editor'; @@ -47,6 +47,7 @@ describe('EditorStateHolder', () => { }); service = TestBed.inject(EditorStateHolder); service.flowsService = flowsServiceSpy as any; + flowsServiceSpy.getFlowValidation.mockReturnValue(of([])); }); it('should be created', () => { @@ -61,11 +62,11 @@ describe('EditorStateHolder', () => { describe('openDocument', () => { it('should set the current flow', async () => { const flow = makeFlow(); - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); await service.openDocument(flow); - expect(service.currentFlow()).toEqual(flow); + expect(service.currentFlow()).toMatchObject(flow); + expect(service.currentFlow()?.validationErrors).toEqual([]); expect(service.hasFlow()).toBe(true); expect(service.isDirty()).toBe(false); }); @@ -73,7 +74,6 @@ describe('EditorStateHolder', () => { it('should prompt confirmation when dirty', async () => { const flow1 = makeFlow({ id: 'f1' }); const flow2 = makeFlow({ id: 'f2' }); - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); await service.openDocument(flow1); service.updateData({ @@ -94,7 +94,6 @@ describe('EditorStateHolder', () => { it('should skip dirty check when option is set', async () => { const flow1 = makeFlow({ id: 'f1' }); const flow2 = makeFlow({ id: 'f2' }); - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); await service.openDocument(flow1); service.updateData({ @@ -113,8 +112,6 @@ describe('EditorStateHolder', () => { describe('closeDocument', () => { it('should clear the current flow', async () => { - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); - await service.openDocument(makeFlow()); service.closeDocument(); @@ -126,8 +123,6 @@ describe('EditorStateHolder', () => { describe('updateData', () => { it('should mark editor as dirty', async () => { - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); - await service.openDocument(makeFlow()); const newData: FlowData = { blocks: [{ id: 'b1', name: 'Block1', inputs: [], outputs: [], specificConfiguration: {}, typeName: 'LLMBlock' }], @@ -142,7 +137,6 @@ describe('EditorStateHolder', () => { it('should not mark dirty if data unchanged', async () => { const flow = makeFlow(); - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); await service.openDocument(flow); service.updateData({ ...flow.data }); @@ -153,24 +147,18 @@ describe('EditorStateHolder', () => { describe('isCurrentFlowReadOnly', () => { it('should return true for finalized flow', async () => { - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); - await service.openDocument(makeFlow({ finalized: true })); expect(service.isCurrentFlowReadOnly()).toBe(true); }); it('should return true for public flow by another author', async () => { - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); - await service.openDocument(makeFlow({ visibility: 'PUBLIC', author: 'otheruser' })); expect(service.isCurrentFlowReadOnly()).toBe(true); }); it('should return false for own private flow', async () => { - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); - await service.openDocument(makeFlow({ visibility: 'PRIVATE', author: 'testuser' })); expect(service.isCurrentFlowReadOnly()).toBe(false); @@ -198,8 +186,6 @@ describe('EditorStateHolder', () => { describe('updateFlowTitle', () => { it('should update the title and mark dirty', async () => { - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); - await service.openDocument(makeFlow({ name: 'Old Title' })); service.updateFlowTitle('New Title'); @@ -208,8 +194,6 @@ describe('EditorStateHolder', () => { }); it('should not mark dirty if title unchanged', async () => { - flowsServiceSpy.getFlowValidation.mockReturnValue({ subscribe: () => {} } as any); - await service.openDocument(makeFlow({ name: 'Same' })); service.updateFlowTitle('Same');