Strengthen frontend spec coverage

This commit is contained in:
Lucio Lelii 2026-04-21 12:39:33 +02:00
parent a090285bf7
commit c87596fea1
13 changed files with 349 additions and 34 deletions

View File

@ -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();
});
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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);

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

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