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