import { describe, it, expect, vi, beforeEach } from 'vitest'; import { FloorplanImporter } from '../src/floorplan-import.js'; // Minimal renderer mock function makeRenderer() { const listeners = {}; return { houseData: null, currentFloor: 0, container: { dispatchEvent: vi.fn(), addEventListener: (type, fn) => { listeners[type] = listeners[type] || []; listeners[type].push(fn); }, _listeners: listeners }, _clearFloor: vi.fn(), _renderRoom: vi.fn(), showFloor: vi.fn() }; } // Valid house data for testing function makeSampleHouse() { return { name: 'Test House', description: 'A test house', units: 'meters', building: { footprint: { width: 10, depth: 8 }, wallThickness: 0.24, roofType: 'gable' }, floors: [{ id: 'eg', name: 'Ground Floor', nameEN: 'Ground Floor', level: 0, ceilingHeight: 2.6, rooms: [ { id: 'eg-living', name: 'Living Room', nameEN: 'Living Room', type: 'living', position: { x: 0, y: 0 }, dimensions: { width: 5, length: 4 }, flooring: 'hardwood', walls: { south: { type: 'exterior', doors: [], windows: [] }, north: { type: 'interior', doors: [{ id: 'eg-living-d1', type: 'interior', position: 1, width: 0.9, height: 2.1, connectsTo: 'eg-kitchen' }], windows: [] }, east: { type: 'interior', doors: [], windows: [] }, west: { type: 'exterior', doors: [], windows: [{ id: 'eg-living-w1', type: 'casement', position: 1.5, width: 1.2, height: 1.2, sillHeight: 0.9 }] } } }, { id: 'eg-kitchen', name: 'Kitchen', nameEN: 'Kitchen', type: 'kitchen', position: { x: 0, y: 4 }, dimensions: { width: 5, length: 4 }, flooring: 'tile', walls: { south: { type: 'interior', doors: [], windows: [] }, north: { type: 'exterior', doors: [], windows: [] }, east: { type: 'interior', doors: [], windows: [] }, west: { type: 'exterior', doors: [], windows: [] } } } ] }] }; } describe('FloorplanImporter', () => { let renderer; let importer; beforeEach(() => { renderer = makeRenderer(); importer = new FloorplanImporter(renderer); }); describe('_validateHouseJSON', () => { it('validates correct house data', () => { const result = importer._validateHouseJSON(makeSampleHouse()); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('catches missing name', () => { const data = makeSampleHouse(); delete data.name; const result = importer._validateHouseJSON(data); expect(result.valid).toBe(false); expect(result.errors).toContain('Missing building name'); }); it('catches missing building footprint', () => { const data = makeSampleHouse(); delete data.building.footprint; const result = importer._validateHouseJSON(data); expect(result.valid).toBe(false); expect(result.errors).toContain('Missing building footprint'); }); it('catches missing floors', () => { const data = makeSampleHouse(); data.floors = []; const result = importer._validateHouseJSON(data); expect(result.valid).toBe(false); expect(result.errors).toContain('No floors found'); }); it('catches rooms with missing walls', () => { const data = makeSampleHouse(); delete data.floors[0].rooms[0].walls; const result = importer._validateHouseJSON(data); expect(result.valid).toBe(false); expect(result.errors).toContain('Room "eg-living" missing walls'); }); it('catches invalid wall type', () => { const data = makeSampleHouse(); data.floors[0].rooms[0].walls.south.type = 'concrete'; const result = importer._validateHouseJSON(data); expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes('invalid type'))).toBe(true); }); }); describe('_autoRepair', () => { it('adds missing name', () => { const data = { floors: [{ rooms: [] }] }; const repaired = importer._autoRepair(data); expect(repaired.name).toBe('Imported Floor Plan'); }); it('adds missing units', () => { const data = { floors: [] }; const repaired = importer._autoRepair(data); expect(repaired.units).toBe('meters'); }); it('converts string numbers to floats', () => { const data = { floors: [{ rooms: [{ id: 'test', position: { x: '3.5', y: '2.0' }, dimensions: { width: '4', length: '5.5' }, walls: { north: { type: 'exterior', doors: [], windows: [] }, south: { type: 'exterior', doors: [], windows: [] }, east: { type: 'interior', doors: [], windows: [] }, west: { type: 'interior', doors: [], windows: [] } } }] }] }; const repaired = importer._autoRepair(data); const room = repaired.floors[0].rooms[0]; expect(room.position.x).toBe(3.5); expect(room.position.y).toBe(2.0); expect(room.dimensions.width).toBe(4); expect(room.dimensions.length).toBe(5.5); }); it('generates missing IDs', () => { const data = { floors: [{ id: 'eg', rooms: [{ nameEN: 'Living Room', position: { x: 0, y: 0 }, dimensions: { width: 4, length: 4 }, walls: {} }] }] }; const repaired = importer._autoRepair(data); expect(repaired.floors[0].rooms[0].id).toBe('eg-living-room'); }); it('infers flooring from room type', () => { const data = { floors: [{ id: 'eg', rooms: [ { id: 'eg-k', type: 'kitchen', position: { x: 0, y: 0 }, dimensions: { width: 3, length: 3 }, walls: {} }, { id: 'eg-b', type: 'bedroom', position: { x: 3, y: 0 }, dimensions: { width: 3, length: 3 }, walls: {} } ] }] }; const repaired = importer._autoRepair(data); expect(repaired.floors[0].rooms[0].flooring).toBe('tile'); expect(repaired.floors[0].rooms[1].flooring).toBe('hardwood'); }); it('adds missing walls', () => { const data = { floors: [{ rooms: [{ id: 'test', position: { x: 0, y: 0 }, dimensions: { width: 3, length: 3 } }] }] }; const repaired = importer._autoRepair(data); const room = repaired.floors[0].rooms[0]; expect(room.walls.north).toBeDefined(); expect(room.walls.south).toBeDefined(); expect(room.walls.east).toBeDefined(); expect(room.walls.west).toBeDefined(); expect(room.walls.north.type).toBe('interior'); }); it('computes footprint from rooms when missing', () => { const data = { floors: [{ rooms: [ { id: 'r1', position: { x: 0, y: 0 }, dimensions: { width: 5, length: 4 }, walls: {} }, { id: 'r2', position: { x: 5, y: 0 }, dimensions: { width: 3, length: 4 }, walls: {} } ] }] }; const repaired = importer._autoRepair(data); expect(repaired.building.footprint.width).toBe(8); expect(repaired.building.footprint.depth).toBe(4); }); }); describe('_applyToRenderer', () => { it('sets house data on renderer', () => { const data = makeSampleHouse(); importer._applyToRenderer(data); expect(renderer.houseData).toBe(data); expect(renderer.currentFloor).toBe(0); }); it('clears and renders floor', () => { const data = makeSampleHouse(); importer._applyToRenderer(data); expect(renderer._clearFloor).toHaveBeenCalled(); expect(renderer._renderRoom).toHaveBeenCalledTimes(2); }); it('dispatches houseloaded event', () => { const data = makeSampleHouse(); importer._applyToRenderer(data); expect(renderer.container.dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ type: 'houseloaded', detail: { name: 'Test House', floors: 1 } }) ); }); it('calls onHouseLoaded callback', () => { const callback = vi.fn(); const importerWithCb = new FloorplanImporter(renderer, { onHouseLoaded: callback }); const data = makeSampleHouse(); importerWithCb._applyToRenderer(data); expect(callback).toHaveBeenCalledWith(data); }); }); describe('_buildPrompt', () => { it('includes building name', () => { const prompt = importer._buildPrompt('My House', 1, ''); expect(prompt).toContain('My House'); }); it('includes scale hint when provided', () => { const prompt = importer._buildPrompt('House', 1, 'Living room is 5m wide'); expect(prompt).toContain('Scale reference: Living room is 5m wide'); }); it('uses default scale message when no hint', () => { const prompt = importer._buildPrompt('House', 1, ''); expect(prompt).toContain('Estimate dimensions from standard door widths'); }); it('includes floor count', () => { const prompt = importer._buildPrompt('House', 2, ''); expect(prompt).toContain('2 floor(s)'); }); }); });