Add test suite with 132 unit tests across all modules

Covers DesignState (40 tests), HouseRenderer (19), InteractionManager (24),
ThemeManager (8), ExportManager (11), and CatalogPanel (30). Uses vitest
with THREE.js mocks for browser-free testing.
This commit is contained in:
m
2026-02-07 16:34:36 +01:00
parent bc94d41f2b
commit 8ac5b3f1f9
13 changed files with 2058 additions and 0 deletions

260
tests/renderer.test.js Normal file
View File

@@ -0,0 +1,260 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { HouseRenderer, COLORS } from '../src/renderer.js';
// Mock container with minimal DOM-like interface
function makeContainer() {
const listeners = {};
return {
clientWidth: 800,
clientHeight: 600,
appendChild: vi.fn(),
dispatchEvent: vi.fn(),
addEventListener: (type, fn) => {
listeners[type] = listeners[type] || [];
listeners[type].push(fn);
},
removeEventListener: vi.fn(),
_listeners: listeners,
_fireEvent: (type, detail) => {
for (const fn of listeners[type] || []) fn({ detail });
}
};
}
describe('HouseRenderer', () => {
let renderer;
beforeEach(() => {
// Stub global APIs used by the constructor
globalThis.window = globalThis.window || {};
globalThis.window.addEventListener = vi.fn();
globalThis.window.devicePixelRatio = 1;
globalThis.requestAnimationFrame = vi.fn();
// Stub document.createElement for canvas room labels
globalThis.document = globalThis.document || {};
globalThis.document.createElement = (tag) => {
if (tag === 'canvas') {
return {
getContext: () => ({
font: '',
measureText: () => ({ width: 100 }),
fillStyle: '',
fillRect: () => {},
textBaseline: '',
textAlign: '',
fillText: () => {}
}),
width: 0,
height: 0
};
}
return { addEventListener: vi.fn(), click: vi.fn(), type: '', accept: '', files: [] };
};
renderer = new HouseRenderer(makeContainer());
});
describe('COLORS export', () => {
it('exports expected color structure', () => {
expect(COLORS.wall).toHaveProperty('exterior');
expect(COLORS.wall).toHaveProperty('interior');
expect(COLORS.floor).toHaveProperty('tile');
expect(COLORS.floor).toHaveProperty('hardwood');
expect(COLORS).toHaveProperty('ceiling');
expect(COLORS).toHaveProperty('door');
expect(COLORS).toHaveProperty('window');
expect(COLORS).toHaveProperty('windowFrame');
expect(COLORS).toHaveProperty('grid');
expect(COLORS).toHaveProperty('selected');
});
});
describe('initial state', () => {
it('has null data references', () => {
expect(renderer.houseData).toBeNull();
expect(renderer.catalogData).toBeNull();
expect(renderer.designData).toBeNull();
});
it('starts on floor 0', () => {
expect(renderer.currentFloor).toBe(0);
});
it('has empty mesh maps', () => {
expect(renderer.roomMeshes.size).toBe(0);
expect(renderer.furnitureMeshes.size).toBe(0);
});
});
describe('_computeWallSegments', () => {
it('returns single full-height segment with no openings', () => {
const segments = renderer._computeWallSegments([], 4, 2.6);
expect(segments).toHaveLength(1);
expect(segments[0]).toEqual({ w: 4, h: 2.6, cx: 2, cy: 1.3 });
});
it('creates segments around a door opening', () => {
const openings = [{ position: 1, width: 1, height: 2.1, bottom: 0 }];
const segments = renderer._computeWallSegments(openings, 4, 2.6);
// Left of door
const left = segments.find(s => s.cx < 1);
expect(left).toBeDefined();
expect(left.w).toBeCloseTo(1, 1);
expect(left.h).toBe(2.6);
// Above door
const above = segments.find(s => s.cy > 2.1);
expect(above).toBeDefined();
expect(above.w).toBe(1); // door width
expect(above.h).toBeCloseTo(0.5, 1);
// Right of door
const right = segments.find(s => s.cx > 2);
expect(right).toBeDefined();
expect(right.w).toBeCloseTo(2, 1);
});
it('creates segments around a window with sill', () => {
const openings = [{
position: 1, width: 1.2, height: 1.0,
bottom: 0.8, sillHeight: 0.8
}];
const segments = renderer._computeWallSegments(openings, 4, 2.6);
// Should have: left, above, below (sill), right
expect(segments.length).toBeGreaterThanOrEqual(4);
// Below-sill segment
const below = segments.find(s => s.h === 0.8 && s.w === 1.2);
expect(below).toBeDefined();
});
it('handles multiple openings', () => {
const openings = [
{ position: 0.5, width: 0.8, height: 2.1, bottom: 0 },
{ position: 2.5, width: 1.0, height: 1.0, bottom: 0.8 }
];
const segments = renderer._computeWallSegments(openings, 5, 2.6);
// Should have segments between and around both openings
expect(segments.length).toBeGreaterThanOrEqual(4);
});
});
describe('getFloors / getRooms', () => {
it('getFloors returns empty array with no house data', () => {
expect(renderer.getFloors()).toEqual([]);
});
it('getRooms returns empty array with no house data', () => {
expect(renderer.getRooms()).toEqual([]);
});
it('getFloors returns floor list after loading', () => {
renderer.houseData = {
floors: [
{ id: 'eg', name: 'Erdgeschoss', nameEN: 'Ground Floor', rooms: [] },
{ id: 'og', name: 'Obergeschoss', nameEN: 'Upper Floor', rooms: [] }
]
};
const floors = renderer.getFloors();
expect(floors).toHaveLength(2);
expect(floors[0]).toEqual({ index: 0, id: 'eg', name: 'Erdgeschoss', nameEN: 'Ground Floor' });
});
it('getRooms returns rooms for current floor', () => {
renderer.houseData = {
floors: [{
id: 'eg', name: 'EG', rooms: [
{ id: 'r1', name: 'Room1', nameEN: 'R1', type: 'living', dimensions: { width: 4, length: 5 } },
{ id: 'r2', name: 'Room2', nameEN: 'R2', type: 'bedroom', dimensions: { width: 3, length: 3 } }
]
}]
};
renderer.currentFloor = 0;
const rooms = renderer.getRooms();
expect(rooms).toHaveLength(2);
expect(rooms[0].id).toBe('r1');
expect(rooms[0].area).toBe(20);
expect(rooms[1].area).toBe(9);
});
it('getRooms handles invalid floor index', () => {
renderer.houseData = { floors: [] };
renderer.currentFloor = 5;
expect(renderer.getRooms()).toEqual([]);
});
});
describe('setControlsEnabled', () => {
it('toggles controls.enabled', () => {
renderer.setControlsEnabled(false);
expect(renderer.controls.enabled).toBe(false);
renderer.setControlsEnabled(true);
expect(renderer.controls.enabled).toBe(true);
});
});
describe('showFloor', () => {
it('updates currentFloor', () => {
renderer.houseData = {
floors: [
{ id: 'eg', name: 'EG', ceilingHeight: 2.6, rooms: [] },
{ id: 'og', name: 'OG', ceilingHeight: 2.5, rooms: [] }
]
};
renderer.showFloor(1);
expect(renderer.currentFloor).toBe(1);
});
});
describe('_buildFurnitureMesh', () => {
it('returns group for catalog item with box parts', () => {
const item = {
id: 'test',
mesh: {
type: 'group',
parts: [
{ geometry: 'box', size: [1, 0.5, 0.5], position: [0, 0.25, 0], color: '#8b4513' }
]
}
};
const group = renderer._buildFurnitureMesh(item);
expect(group.children).toHaveLength(1);
});
it('returns group for cylinder parts', () => {
const item = {
id: 'test',
mesh: {
type: 'group',
parts: [
{ geometry: 'cylinder', radius: 0.1, height: 0.7, position: [0, 0.35, 0], color: '#333' }
]
}
};
const group = renderer._buildFurnitureMesh(item);
expect(group.children).toHaveLength(1);
});
it('returns empty group for missing mesh def', () => {
const group = renderer._buildFurnitureMesh({ id: 'no-mesh' });
expect(group.children).toHaveLength(0);
});
it('skips unknown geometry types', () => {
const item = {
id: 'test',
mesh: {
type: 'group',
parts: [
{ geometry: 'sphere', radius: 0.5, position: [0, 0, 0], color: '#fff' }
]
}
};
const group = renderer._buildFurnitureMesh(item);
expect(group.children).toHaveLength(0);
});
});
});