This commit is contained in:
Bastian Wagner
2026-07-20 09:01:36 +02:00
parent 8cf57d7878
commit e62673ac11
98 changed files with 17372 additions and 80 deletions

View File

@@ -0,0 +1,304 @@
import { provideHttpClient } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { vi } from 'vitest';
import { AuthService } from '../../core/auth.service';
import { FurnitureGridComponent } from './furniture-grid.component';
import type {
FurnitureOption,
FurnitureRequirement,
FurnitureScenario,
PageResult,
} from './hauspilot-api.service';
import { HauspilotApiService } from './hauspilot-api.service';
const requirement: FurnitureRequirement = {
id: 'requirement-1',
version: 2,
createdAt: '2026-01-01',
updatedAt: '2026-01-02',
projectId: 'project-1',
roomId: 'room-1',
name: 'Sofa',
description: null,
category: 'seating',
priority: 'high',
requiredQuantity: 1,
status: 'decision_open',
responsibleUserId: null,
maximumBudget: '2000.00',
sortOrder: 0,
options: [],
optionCount: 0,
};
const page: PageResult<FurnitureRequirement> = {
items: [requirement],
page: 1,
pageSize: 50,
totalItems: 1,
totalPages: 1,
};
describe('FurnitureGridComponent', () => {
it('uses stable database IDs and passes paging and room filters to the API', async () => {
const calls: Array<Record<string, string | number | boolean | readonly string[]>> = [];
const api = {
furnitureRequirements: (
_id: string,
query: Record<string, string | number | boolean | readonly string[]>,
) => {
calls.push(query);
return of(page);
},
furnitureProjectOptions: () => of({ ...page, items: [] }),
};
await TestBed.configureTestingModule({
imports: [FurnitureGridComponent],
providers: [
provideHttpClient(),
{ provide: HauspilotApiService, useValue: api },
{ provide: AuthService, useValue: { user: () => ({ id: 'user-1' }) } },
],
}).compileComponents();
const component = TestBed.createComponent(FurnitureGridComponent).componentInstance;
component.projectId = 'project-1';
component.roomId = 'room-1';
component.load(2);
expect(component.getRowId({ data: requirement })).toBe('requirement-1');
expect(calls[0]).toMatchObject({ page: 2, pageSize: 50, roomId: 'room-1' });
});
it('debounces search requests', () => {
vi.useFakeTimers();
let calls = 0;
const api = {
furnitureRequirements: () => {
calls += 1;
return of(page);
},
furnitureProjectOptions: () => of({ ...page, items: [] }),
};
TestBed.configureTestingModule({
imports: [FurnitureGridComponent],
providers: [
provideHttpClient(),
{ provide: HauspilotApiService, useValue: api },
{ provide: AuthService, useValue: { user: () => ({ id: 'user-1' }) } },
],
});
const component = TestBed.createComponent(FurnitureGridComponent).componentInstance;
component.projectId = 'project-1';
component.searchChanges.next('So');
vi.advanceTimersByTime(150);
component.searchChanges.next('Sofa');
vi.advanceTimersByTime(299);
expect(calls).toBe(0);
vi.advanceTimersByTime(1);
expect(calls).toBe(1);
vi.useRealTimers();
});
it('does not expose editable requirement cells to readers', async () => {
await TestBed.configureTestingModule({
imports: [FurnitureGridComponent],
providers: [
provideHttpClient(),
{
provide: HauspilotApiService,
useValue: {
furnitureRequirements: () => of(page),
furnitureProjectOptions: () => of({ ...page, items: [] }),
},
},
{ provide: AuthService, useValue: { user: () => ({ id: 'reader-1' }) } },
],
}).compileComponents();
const component = TestBed.createComponent(FurnitureGridComponent).componentInstance;
component.canEdit = false;
component.ngOnChanges();
expect(component.columns().filter((column) => column.editable === true)).toHaveLength(0);
});
it('uses select editors and readable icon labels for priority and status', async () => {
await TestBed.configureTestingModule({
imports: [FurnitureGridComponent],
providers: [
provideHttpClient(),
{
provide: HauspilotApiService,
useValue: {
furnitureRequirements: () => of(page),
furnitureProjectOptions: () => of({ ...page, items: [] }),
},
},
{ provide: AuthService, useValue: { user: () => ({ id: 'editor-1' }) } },
],
}).compileComponents();
const component = TestBed.createComponent(FurnitureGridComponent).componentInstance;
component.canEdit = true;
component.ngOnChanges();
const priority = component.columns().find((column) => column.field === 'priority');
const status = component.columns().find((column) => column.field === 'status');
expect(priority?.cellEditor).toBe('agSelectCellEditor');
expect(status?.cellEditor).toBe('agSelectCellEditor');
expect(priority?.cellEditorParams).toMatchObject({
values: ['optional', 'low', 'normal', 'high', 'essential'],
});
expect(status?.cellEditorParams).toMatchObject({
values: [
'identified',
'research',
'has_options',
'decision_open',
'selected',
'ordered',
'partially_delivered',
'delivered',
'assembled',
'omitted',
],
});
expect(
typeof priority?.valueFormatter === 'function'
? priority.valueFormatter({ value: 'essential' } as never)
: '',
).toBe('◆ Unverzichtbar');
expect(
typeof status?.valueFormatter === 'function'
? status.valueFormatter({ value: 'decision_open' } as never)
: '',
).toBe('? Entscheidung offen');
});
it('falls back to a supported server sort when persisted grid state is invalid', async () => {
const calls: Array<Record<string, string | number | boolean | readonly string[]>> = [];
const api = {
furnitureRequirements: (
_id: string,
query: Record<string, string | number | boolean | readonly string[]>,
) => {
calls.push(query);
return of(page);
},
furnitureProjectOptions: () => of({ ...page, items: [] }),
};
await TestBed.configureTestingModule({
imports: [FurnitureGridComponent],
providers: [
provideHttpClient(),
{ provide: HauspilotApiService, useValue: api },
{ provide: AuthService, useValue: { user: () => ({ id: 'user-1' }) } },
],
}).compileComponents();
const component = TestBed.createComponent(FurnitureGridComponent).componentInstance;
component.projectId = 'project-1';
Object.defineProperty(component, 'gridApi', {
value: { getColumnState: () => [{ colId: 'optionCount', sort: 'asc' }] },
});
component.load(1);
expect(calls[0]?.['sortBy']).toBe('sortOrder');
});
it('assigns an option to a scenario through the existing selection endpoint', async () => {
const option: FurnitureOption = {
id: 'option-1',
version: 1,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
projectId: 'project-1',
requirementId: requirement.id,
name: 'Sofa Wunsch',
manufacturer: null,
model: null,
description: null,
retailer: null,
productUrl: null,
articleNumber: null,
unitPrice: '1000.00',
originalPrice: null,
shippingCost: '0.00',
additionalCost: '0.00',
discount: '0.00',
totalPrice: '1000.00',
currency: 'EUR',
quantity: 1,
width: null,
height: null,
depth: null,
weight: null,
color: null,
material: null,
deliveryDays: null,
expectedDeliveryDate: null,
availability: 'available',
favorite: true,
currentlySelected: false,
status: 'favorite',
notes: null,
budgetCategoryId: null,
existingItem: false,
movingCost: '0.00',
refurbishmentCost: '0.00',
deliveryStatus: 'not_ordered',
deliveredQuantity: 0,
orderNumber: null,
orderedAt: null,
};
const scenario: FurnitureScenario = {
id: 'scenario-1',
version: 3,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
projectId: 'project-1',
name: 'Wunsch',
description: null,
type: 'preferred',
status: 'active',
isDefault: true,
total: '0.00',
selectedRequirements: 0,
openRequirements: 1,
byRoom: {},
selections: [],
};
const updateSelections = vi.fn(() =>
of({
...scenario,
version: 4,
total: option.totalPrice,
selections: [
{ requirementId: requirement.id, optionId: option.id, quantity: option.quantity },
],
}),
);
await TestBed.configureTestingModule({
imports: [FurnitureGridComponent],
providers: [
provideHttpClient(),
{
provide: HauspilotApiService,
useValue: {
furnitureRequirements: () => of(page),
furnitureProjectOptions: () => of({ ...page, items: [] }),
updateFurnitureScenarioSelections: updateSelections,
},
},
{ provide: AuthService, useValue: { user: () => ({ id: 'editor-1' }) } },
],
}).compileComponents();
const component = TestBed.createComponent(FurnitureGridComponent).componentInstance;
component.projectId = 'project-1';
component.canEdit = true;
component.scenarios = [scenario];
component.cellChanged({
oldValue: '',
newValue: option.id,
data: { ...requirement, options: [option] },
column: { getColId: () => `scenario:${scenario.id}` },
node: { setDataValue: vi.fn() },
} as never);
expect(updateSelections).toHaveBeenCalledWith('project-1', scenario, [
{ requirementId: requirement.id, optionId: option.id, quantity: 1 },
]);
});
});