Files
house-design/tests/export.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

191 lines
6.5 KiB
JavaScript

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