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:
190
tests/export.test.js
Normal file
190
tests/export.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user