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.
261 lines
8.1 KiB
JavaScript
261 lines
8.1 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|