Files
house-design/tests/catalog.test.js
m 8ac5b3f1f9 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.
2026-02-07 16:34:36 +01:00

373 lines
12 KiB
JavaScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CatalogPanel } from '../src/catalog.js';
function makeCatalogData() {
return {
categories: ['seating', 'tables', 'storage'],
items: [
{
id: 'chair-1', name: 'Dining Chair', category: 'seating',
dimensions: { width: 0.45, depth: 0.5, height: 0.9 },
rooms: ['wohnzimmer'],
mesh: { type: 'group', parts: [{ color: '#8b4513' }] }
},
{
id: 'table-1', name: 'Kitchen Table', category: 'tables',
dimensions: { width: 1.4, depth: 0.8, height: 0.75 },
rooms: ['küche'],
mesh: { type: 'group', parts: [{ color: '#d2b48c' }] }
},
{
id: 'ikea-shelf-1', name: 'KALLAX Shelf', category: 'storage',
dimensions: { width: 0.77, depth: 0.39, height: 1.47 },
rooms: [],
ikeaSeries: 'KALLAX',
mesh: { type: 'group', parts: [{ color: '#ffffff' }] }
},
{
id: 'ikea-chair-1', name: 'POÄNG Chair', category: 'seating',
dimensions: { width: 0.68, depth: 0.82, height: 1.0 },
rooms: ['wohnzimmer'],
ikeaSeries: 'POÄNG',
mesh: { type: 'group', parts: [{ color: '#b5651d' }] }
}
]
};
}
function makeMockRenderer(catalogData) {
const listeners = {};
return {
catalogData: catalogData || makeCatalogData(),
_catalogIndex: new Map((catalogData || makeCatalogData()).items.map(i => [i.id, i])),
container: {
addEventListener: (t, fn) => { listeners[t] = listeners[t] || []; listeners[t].push(fn); },
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
_listeners: listeners
},
houseData: {
floors: [{
id: 'eg',
rooms: [{
id: 'eg-wohnzimmer', name: 'Wohnzimmer', type: 'living',
position: { x: 0, y: 0 },
dimensions: { width: 5, length: 4 }
}]
}]
},
currentFloor: 0,
getRooms: () => [
{ id: 'eg-wohnzimmer', name: 'Wohnzimmer', nameEN: 'Living Room', type: 'living', area: 20 }
]
};
}
function makeMockState() {
return {
addFurniture: vi.fn(() => 0),
onChange: vi.fn(() => () => {})
};
}
function makeMockInteraction() {
return {
selectedRoomId: null,
onChange: vi.fn(() => () => {})
};
}
function makeContainer() {
// Minimal DOM element mock
const el = {
innerHTML: '',
className: '',
style: {},
children: [],
querySelectorAll: () => [],
querySelector: (sel) => {
if (sel === '.catalog-count') return { textContent: '' };
return null;
},
appendChild: vi.fn(function(child) { this.children.push(child); return child; }),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dataset: {}
};
return el;
}
// Create a DOM element factory for jsdom-free testing
function setupDomMock() {
const elements = [];
globalThis.document = {
createElement: (tag) => {
const el = {
tagName: tag.toUpperCase(),
className: '',
innerHTML: '',
textContent: '',
style: {},
dataset: {},
value: '',
type: '',
placeholder: '',
step: '',
min: '',
max: '',
children: [],
_listeners: {},
appendChild: vi.fn(function(child) { this.children.push(child); return child; }),
addEventListener: vi.fn(function(type, fn) {
this._listeners[type] = this._listeners[type] || [];
this._listeners[type].push(fn);
}),
removeEventListener: vi.fn(),
querySelector: function(sel) {
if (sel === '.catalog-count') return { textContent: '' };
if (sel === '.catalog-item-add') return { addEventListener: vi.fn(), removeEventListener: vi.fn() };
return null;
},
querySelectorAll: () => [],
focus: vi.fn(),
click: vi.fn()
};
elements.push(el);
return el;
}
};
return elements;
}
describe('CatalogPanel', () => {
let panel, container, mockRenderer, mockState, mockInteraction;
beforeEach(() => {
setupDomMock();
container = makeContainer();
mockRenderer = makeMockRenderer();
mockState = makeMockState();
mockInteraction = makeMockInteraction();
panel = new CatalogPanel(container, {
renderer: mockRenderer,
state: mockState,
interaction: mockInteraction
});
});
describe('initialization', () => {
it('sets default filter state', () => {
expect(panel.selectedSource).toBe('all');
expect(panel.selectedCategory).toBe('all');
expect(panel.selectedSeries).toBe('all');
expect(panel.searchQuery).toBe('');
});
it('stores renderer, state, and interaction references', () => {
expect(panel.renderer).toBe(mockRenderer);
expect(panel.state).toBe(mockState);
expect(panel.interaction).toBe(mockInteraction);
});
});
describe('_hasIkeaItems', () => {
it('returns true when IKEA items exist', () => {
expect(panel._hasIkeaItems()).toBe(true);
});
it('returns false with no IKEA items', () => {
mockRenderer.catalogData.items = mockRenderer.catalogData.items.filter(i => !i.id.startsWith('ikea-'));
expect(panel._hasIkeaItems()).toBe(false);
});
it('returns false with no catalog', () => {
mockRenderer.catalogData = null;
expect(panel._hasIkeaItems()).toBe(false);
});
});
describe('_getSourceFilteredItems', () => {
it('returns all items for "all" source', () => {
panel.selectedSource = 'all';
expect(panel._getSourceFilteredItems()).toHaveLength(4);
});
it('returns only IKEA items for "ikea" source', () => {
panel.selectedSource = 'ikea';
const items = panel._getSourceFilteredItems();
expect(items).toHaveLength(2);
expect(items.every(i => i.id.startsWith('ikea-'))).toBe(true);
});
it('returns only standard items for "standard" source', () => {
panel.selectedSource = 'standard';
const items = panel._getSourceFilteredItems();
expect(items).toHaveLength(2);
expect(items.every(i => !i.id.startsWith('ikea-'))).toBe(true);
});
it('returns empty array with no catalog', () => {
mockRenderer.catalogData = null;
expect(panel._getSourceFilteredItems()).toEqual([]);
});
});
describe('_getFilteredItems', () => {
it('filters by category', () => {
panel.selectedCategory = 'seating';
const items = panel._getFilteredItems();
expect(items).toHaveLength(2); // chair-1 + ikea-chair-1
expect(items.every(i => i.category === 'seating')).toBe(true);
});
it('filters by source + category', () => {
panel.selectedSource = 'ikea';
panel.selectedCategory = 'seating';
const items = panel._getFilteredItems();
expect(items).toHaveLength(1);
expect(items[0].id).toBe('ikea-chair-1');
});
it('filters by series', () => {
panel.selectedSource = 'ikea';
panel.selectedSeries = 'KALLAX';
const items = panel._getFilteredItems();
expect(items).toHaveLength(1);
expect(items[0].id).toBe('ikea-shelf-1');
});
it('filters by search query', () => {
panel.searchQuery = 'kallax';
const items = panel._getFilteredItems();
expect(items).toHaveLength(1);
expect(items[0].id).toBe('ikea-shelf-1');
});
it('search matches on name, id, and category', () => {
panel.searchQuery = 'chair';
const items = panel._getFilteredItems();
expect(items.length).toBeGreaterThanOrEqual(2); // chair-1, ikea-chair-1
});
it('search is case-insensitive', () => {
panel.searchQuery = 'DINING';
const items = panel._getFilteredItems();
expect(items).toHaveLength(1);
expect(items[0].id).toBe('chair-1');
});
it('combined filters narrow results', () => {
panel.selectedSource = 'standard';
panel.selectedCategory = 'tables';
panel.searchQuery = 'kitchen';
const items = panel._getFilteredItems();
expect(items).toHaveLength(1);
expect(items[0].id).toBe('table-1');
});
it('no matches returns empty array', () => {
panel.searchQuery = 'nonexistent-item-xyz';
expect(panel._getFilteredItems()).toHaveLength(0);
});
});
describe('_getTargetRoom', () => {
it('uses selectedRoomId if set', () => {
panel.selectedRoomId = 'custom-room';
expect(panel._getTargetRoom({})).toBe('custom-room');
});
it('falls back to interaction selectedRoomId', () => {
mockInteraction.selectedRoomId = 'interaction-room';
expect(panel._getTargetRoom({})).toBe('interaction-room');
});
it('matches catalog item room hints to floor rooms', () => {
const item = { rooms: ['wohnzimmer'] };
expect(panel._getTargetRoom(item)).toBe('eg-wohnzimmer');
});
it('falls back to first room when no hint matches', () => {
const item = { rooms: ['bathroom'] };
expect(panel._getTargetRoom(item)).toBe('eg-wohnzimmer');
});
it('falls back to first room with no hints', () => {
expect(panel._getTargetRoom({ rooms: [] })).toBe('eg-wohnzimmer');
});
it('returns null with no rooms available', () => {
mockRenderer.getRooms = () => [];
expect(panel._getTargetRoom({ rooms: [] })).toBeNull();
});
});
describe('_placeItem', () => {
it('calls state.addFurniture with correct placement', () => {
const item = { id: 'chair-1', rooms: ['wohnzimmer'] };
panel._placeItem(item);
expect(mockState.addFurniture).toHaveBeenCalledTimes(1);
const [roomId, placement] = mockState.addFurniture.mock.calls[0];
expect(roomId).toBe('eg-wohnzimmer');
expect(placement.catalogId).toBe('chair-1');
expect(placement.position.x).toBe(2.5); // center of 5m wide room
expect(placement.position.z).toBe(2); // center of 4m long room
expect(placement.rotation).toBe(0);
});
it('does nothing when no target room found', () => {
mockRenderer.getRooms = () => [];
panel._placeItem({ id: 'x', rooms: [] });
expect(mockState.addFurniture).not.toHaveBeenCalled();
});
});
describe('setSelectedRoom', () => {
it('sets selectedRoomId', () => {
panel.setSelectedRoom('room-x');
expect(panel.selectedRoomId).toBe('room-x');
});
it('clears with null', () => {
panel.setSelectedRoom('room-x');
panel.setSelectedRoom(null);
expect(panel.selectedRoomId).toBeNull();
});
});
describe('_addCustomItem', () => {
it('adds item to catalog', () => {
const countBefore = mockRenderer.catalogData.items.length;
panel._addCustomItem({
name: 'My Shelf', width: 1, depth: 0.5, height: 1.5,
color: '#aabbcc', category: 'storage'
});
expect(mockRenderer.catalogData.items.length).toBe(countBefore + 1);
const added = mockRenderer.catalogData.items[countBefore];
expect(added.id).toBe('custom-my-shelf');
expect(added.name).toBe('My Shelf');
expect(added.category).toBe('storage');
expect(added.dimensions).toEqual({ width: 1, depth: 0.5, height: 1.5 });
});
it('generates unique id on collision', () => {
panel._addCustomItem({ name: 'Shelf', width: 1, depth: 0.5, height: 1, color: '#000', category: 'storage' });
panel._addCustomItem({ name: 'Shelf', width: 1, depth: 0.5, height: 1, color: '#000', category: 'storage' });
const customItems = mockRenderer.catalogData.items.filter(i => i.id.startsWith('custom-shelf'));
expect(customItems).toHaveLength(2);
expect(customItems[0].id).not.toBe(customItems[1].id);
});
it('does nothing with no catalog', () => {
mockRenderer.catalogData = null;
panel._addCustomItem({ name: 'X', width: 1, depth: 1, height: 1, color: '#000', category: 'storage' });
// Should not throw
});
});
});