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

190
tests/export.test.js Normal file
View File

@@ -0,0 +1,190 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExportManager } from '../src/export.js';
import { DesignState } from '../src/state.js';
import { Vector2 } from '../tests/__mocks__/three.js';
function makeMockRenderer() {
return {
renderer: {
getSize: vi.fn((v) => v.set(800, 600)),
getPixelRatio: vi.fn(() => 1),
setSize: vi.fn(),
setPixelRatio: vi.fn(),
render: vi.fn(),
domElement: {
toDataURL: vi.fn(() => 'data:image/png;base64,mock')
}
},
camera: {
aspect: 800 / 600,
updateProjectionMatrix: vi.fn()
},
scene: {},
container: {
dispatchEvent: vi.fn()
},
houseData: {
floors: [{ id: 'eg', rooms: [{ id: 'r1', name: 'Room1' }] }]
},
currentFloor: 0,
designData: null,
_clearFloor: vi.fn(),
_renderRoom: vi.fn(),
_placeFurnitureForFloor: vi.fn()
};
}
function makeDesign() {
return {
name: 'Test Design',
rooms: [{ roomId: 'r1', furniture: [{ catalogId: 'c1', position: { x: 1, z: 2 }, rotation: 0 }] }]
};
}
describe('ExportManager', () => {
let exportMgr, state, mockRenderer;
beforeEach(() => {
// Stub DOM APIs
globalThis.URL = { createObjectURL: vi.fn(() => 'blob:url'), revokeObjectURL: vi.fn() };
globalThis.Blob = class Blob { constructor(parts, opts) { this.parts = parts; this.type = opts?.type; } };
globalThis.document = globalThis.document || {};
globalThis.document.createElement = (tag) => {
return { href: '', download: '', click: vi.fn(), type: '', accept: '', files: [], addEventListener: vi.fn() };
};
state = new DesignState(makeDesign());
mockRenderer = makeMockRenderer();
exportMgr = new ExportManager(mockRenderer, state);
});
describe('exportDesignJSON', () => {
it('creates a download with correct filename', () => {
const mockAnchor = { href: '', download: '', click: vi.fn() };
globalThis.document.createElement = () => mockAnchor;
exportMgr.exportDesignJSON();
expect(mockAnchor.download).toBe('Test Design.json');
expect(mockAnchor.click).toHaveBeenCalled();
});
it('includes exportedAt timestamp', () => {
let blobContent = '';
globalThis.Blob = class {
constructor(parts) { blobContent = parts[0]; }
};
globalThis.document.createElement = () => ({ href: '', download: '', click: vi.fn() });
exportMgr.exportDesignJSON();
const data = JSON.parse(blobContent);
expect(data.exportedAt).toBeDefined();
expect(new Date(data.exportedAt).getTime()).not.toBeNaN();
});
it('exports current state data', () => {
let blobContent = '';
globalThis.Blob = class {
constructor(parts) { blobContent = parts[0]; }
};
globalThis.document.createElement = () => ({ href: '', download: '', click: vi.fn() });
exportMgr.exportDesignJSON();
const data = JSON.parse(blobContent);
expect(data.name).toBe('Test Design');
expect(data.rooms).toHaveLength(1);
expect(data.rooms[0].furniture[0].catalogId).toBe('c1');
});
it('uses "design" as fallback filename', () => {
// State with no name
state.loadDesign({ rooms: [] });
const mockAnchor = { href: '', download: '', click: vi.fn() };
globalThis.document.createElement = () => mockAnchor;
exportMgr.exportDesignJSON();
expect(mockAnchor.download).toBe('design.json');
});
});
describe('_loadDesignFile', () => {
it('loads valid design file', async () => {
const design = { name: 'Loaded', rooms: [{ roomId: 'r2', furniture: [] }] };
const file = { text: () => Promise.resolve(JSON.stringify(design)), name: 'loaded.json' };
await exportMgr._loadDesignFile(file);
expect(state.design.name).toBe('Loaded');
expect(state.design.rooms).toHaveLength(1);
});
it('rejects file without rooms array', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const file = { text: () => Promise.resolve('{"name": "bad"}'), name: 'bad.json' };
await exportMgr._loadDesignFile(file);
expect(mockRenderer.container.dispatchEvent).toHaveBeenCalled();
const event = mockRenderer.container.dispatchEvent.mock.calls.find(
c => c[0].type === 'loaderror'
);
expect(event).toBeDefined();
errorSpy.mockRestore();
});
it('rejects invalid JSON', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const file = { text: () => Promise.resolve('not json'), name: 'bad.json' };
await exportMgr._loadDesignFile(file);
expect(mockRenderer.container.dispatchEvent).toHaveBeenCalled();
errorSpy.mockRestore();
});
it('dispatches designloaded event on success', async () => {
const design = { name: 'Success', rooms: [{ roomId: 'r1', furniture: [] }] };
const file = { text: () => Promise.resolve(JSON.stringify(design)), name: 'test.json' };
await exportMgr._loadDesignFile(file);
const event = mockRenderer.container.dispatchEvent.mock.calls.find(
c => c[0].type === 'designloaded'
);
expect(event).toBeDefined();
expect(event[0].detail.name).toBe('Success');
});
});
describe('exportScreenshot', () => {
it('renders at requested resolution', () => {
const mockAnchor = { href: '', download: '', click: vi.fn() };
globalThis.document.createElement = () => mockAnchor;
exportMgr.exportScreenshot(1920, 1080);
expect(mockRenderer.renderer.setSize).toHaveBeenCalledWith(1920, 1080);
expect(mockRenderer.renderer.render).toHaveBeenCalled();
expect(mockAnchor.download).toBe('house-design.png');
});
it('restores original renderer size after capture', () => {
const mockAnchor = { href: '', download: '', click: vi.fn() };
globalThis.document.createElement = () => mockAnchor;
exportMgr.exportScreenshot();
// setSize called twice: once for capture, once to restore
expect(mockRenderer.renderer.setSize).toHaveBeenCalledTimes(2);
// Last call restores original 800x600
const lastCall = mockRenderer.renderer.setSize.mock.calls[1];
expect(lastCall).toEqual([800, 600]);
});
it('updates camera projection matrix twice', () => {
globalThis.document.createElement = () => ({ href: '', download: '', click: vi.fn() });
exportMgr.exportScreenshot();
expect(mockRenderer.camera.updateProjectionMatrix).toHaveBeenCalledTimes(2);
});
});
});