/** * LayerManager Tests – Layer-Verwaltung */ import { describe, it, expect, beforeEach } from 'vitest'; import { LayerManager } from '../src/canvas/LayerManager'; import type { CADLayer } from '../src/types/cad.types'; function makeLayer(id: string, overrides: Partial = {}): CADLayer { return { id, name: id, visible: true, locked: false, color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null, ...overrides, }; } describe('LayerManager', () => { let lm: LayerManager; beforeEach(() => { lm = new LayerManager(); }); describe('addLayer & getLayer', () => { it('should add a layer and retrieve it by id', () => { const layer = makeLayer('layer-1', { name: 'Walls' }); lm.addLayer(layer); expect(lm.getLayer('layer-1')).toBeDefined(); expect(lm.getLayer('layer-1')?.name).toBe('Walls'); }); it('should set first added layer as active', () => { lm.addLayer(makeLayer('layer-1')); expect(lm.getActiveLayerId()).toBe('layer-1'); }); it('should not set active layer if already set', () => { lm.addLayer(makeLayer('layer-1')); lm.addLayer(makeLayer('layer-2')); expect(lm.getActiveLayerId()).toBe('layer-1'); }); it('should return undefined for non-existent layer', () => { expect(lm.getLayer('non-existent')).toBeUndefined(); }); }); describe('removeLayer', () => { it('should remove a layer', () => { lm.addLayer(makeLayer('layer-1')); lm.addLayer(makeLayer('layer-2')); lm.removeLayer('layer-1'); expect(lm.getLayer('layer-1')).toBeUndefined(); expect(lm.getLayers().length).toBe(1); }); it('should switch active layer when removing the active one', () => { lm.addLayer(makeLayer('layer-1')); lm.addLayer(makeLayer('layer-2')); lm.setActiveLayer('layer-1'); lm.removeLayer('layer-1'); expect(lm.getActiveLayerId()).toBe('layer-2'); }); it('should have empty active layer id when removing last layer', () => { lm.addLayer(makeLayer('layer-1')); lm.removeLayer('layer-1'); expect(lm.getActiveLayerId()).toBe(''); }); }); describe('getLayers', () => { it('should return layers sorted by sortOrder', () => { lm.addLayer(makeLayer('layer-3', { sortOrder: 3 })); lm.addLayer(makeLayer('layer-1', { sortOrder: 1 })); lm.addLayer(makeLayer('layer-2', { sortOrder: 2 })); const layers = lm.getLayers(); expect(layers[0].id).toBe('layer-1'); expect(layers[1].id).toBe('layer-2'); expect(layers[2].id).toBe('layer-3'); }); it('should return empty array when no layers', () => { expect(lm.getLayers()).toEqual([]); }); }); describe('getVisibleLayers', () => { it('should return only visible and unlocked layers', () => { lm.addLayer(makeLayer('layer-1', { visible: true, locked: false })); lm.addLayer(makeLayer('layer-2', { visible: false, locked: false })); lm.addLayer(makeLayer('layer-3', { visible: true, locked: true })); lm.addLayer(makeLayer('layer-4', { visible: true, locked: false })); const visible = lm.getVisibleLayers(); expect(visible.length).toBe(2); const ids = visible.map(l => l.id); expect(ids).toContain('layer-1'); expect(ids).toContain('layer-4'); }); }); describe('setActiveLayer & getActiveLayer', () => { it('should set active layer if it exists', () => { lm.addLayer(makeLayer('layer-1')); lm.addLayer(makeLayer('layer-2')); lm.setActiveLayer('layer-2'); expect(lm.getActiveLayerId()).toBe('layer-2'); expect(lm.getActiveLayer()?.id).toBe('layer-2'); }); it('should not set active layer if it does not exist', () => { lm.addLayer(makeLayer('layer-1')); lm.setActiveLayer('non-existent'); expect(lm.getActiveLayerId()).toBe('layer-1'); }); }); describe('toggleVisibility', () => { it('should toggle layer visibility', () => { lm.addLayer(makeLayer('layer-1', { visible: true })); lm.toggleVisibility('layer-1'); expect(lm.getLayer('layer-1')?.visible).toBe(false); lm.toggleVisibility('layer-1'); expect(lm.getLayer('layer-1')?.visible).toBe(true); }); }); describe('toggleLock', () => { it('should toggle layer lock state', () => { lm.addLayer(makeLayer('layer-1', { locked: false })); lm.toggleLock('layer-1'); expect(lm.getLayer('layer-1')?.locked).toBe(true); lm.toggleLock('layer-1'); expect(lm.getLayer('layer-1')?.locked).toBe(false); }); }); describe('clear', () => { it('should clear all layers and reset active', () => { lm.addLayer(makeLayer('layer-1')); lm.addLayer(makeLayer('layer-2')); lm.clear(); expect(lm.getLayers().length).toBe(0); expect(lm.getActiveLayerId()).toBe(''); }); }); describe('renameLayer', () => { it('should rename a layer', () => { lm.addLayer(makeLayer('layer-1', { name: 'Old Name' })); lm.renameLayer('layer-1', 'New Name'); expect(lm.getLayer('layer-1')?.name).toBe('New Name'); }); it('should do nothing for non-existent layer', () => { lm.renameLayer('non-existent', 'New Name'); expect(lm.getLayer('non-existent')).toBeUndefined(); }); }); describe('updateLayer', () => { it('should update multiple layer properties', () => { lm.addLayer(makeLayer('layer-1', { color: '#ffffff', transparency: 0 })); lm.updateLayer('layer-1', { color: '#ff0000', transparency: 50 }); const layer = lm.getLayer('layer-1'); expect(layer?.color).toBe('#ff0000'); expect(layer?.transparency).toBe(50); }); }); describe('setParent', () => { it('should set parent for a layer', () => { lm.addLayer(makeLayer('layer-1')); lm.addLayer(makeLayer('layer-2')); lm.setParent('layer-2', 'layer-1'); expect(lm.getLayer('layer-2')?.parentId).toBe('layer-1'); }); it('should prevent circular references', () => { lm.addLayer(makeLayer('layer-1')); lm.addLayer(makeLayer('layer-2')); lm.setParent('layer-1', 'layer-2'); lm.setParent('layer-2', 'layer-1'); // Would create cycle expect(lm.getLayer('layer-2')?.parentId).toBe(null); }); it('should allow setting parent to null', () => { lm.addLayer(makeLayer('layer-1')); lm.addLayer(makeLayer('layer-2')); lm.setParent('layer-2', 'layer-1'); lm.setParent('layer-2', null); expect(lm.getLayer('layer-2')?.parentId).toBe(null); }); }); describe('getChildLayers & getLayerTree', () => { it('should get child layers of a parent', () => { lm.addLayer(makeLayer('parent', { sortOrder: 0 })); lm.addLayer(makeLayer('child-1', { parentId: 'parent', sortOrder: 1 })); lm.addLayer(makeLayer('child-2', { parentId: 'parent', sortOrder: 2 })); lm.addLayer(makeLayer('other', { sortOrder: 3 })); const children = lm.getChildLayers('parent'); expect(children.length).toBe(2); }); it('should get child layers for null parent (root layers)', () => { lm.addLayer(makeLayer('root-1', { parentId: null })); lm.addLayer(makeLayer('root-2', { parentId: null })); lm.addLayer(makeLayer('child', { parentId: 'root-1' })); const roots = lm.getChildLayers(null); expect(roots.length).toBe(2); }); it('should build a tree structure', () => { lm.addLayer(makeLayer('root', { sortOrder: 0 })); lm.addLayer(makeLayer('child-a', { parentId: 'root', sortOrder: 1 })); lm.addLayer(makeLayer('child-b', { parentId: 'root', sortOrder: 2 })); lm.addLayer(makeLayer('grandchild', { parentId: 'child-a', sortOrder: 3 })); const tree = lm.getLayerTree(); expect(tree.length).toBe(1); expect(tree[0].id).toBe('root'); expect(tree[0].children.length).toBe(2); expect(tree[0].children[0].children.length).toBe(1); expect(tree[0].children[0].children[0].id).toBe('grandchild'); }); }); describe('moveLayer', () => { it('should move layer to new sort order', () => { lm.addLayer(makeLayer('layer-1', { sortOrder: 0 })); lm.moveLayer('layer-1', 5); expect(lm.getLayer('layer-1')?.sortOrder).toBe(5); }); }); describe('duplicateLayer', () => { it('should create a copy of the layer', () => { lm.addLayer(makeLayer('layer-1', { name: 'Original', sortOrder: 1 })); const copy = lm.duplicateLayer('layer-1'); expect(copy).not.toBeNull(); expect(copy?.id).not.toBe('layer-1'); expect(copy?.name).toContain('Kopie'); expect(copy?.sortOrder).toBe(2); expect(lm.getLayers().length).toBe(2); }); it('should return null for non-existent layer', () => { expect(lm.duplicateLayer('non-existent')).toBeNull(); }); }); describe('filterLayers', () => { beforeEach(() => { lm.addLayer(makeLayer('layer-1', { name: 'Walls', visible: true, locked: false, color: '#ff0000' })); lm.addLayer(makeLayer('layer-2', { name: 'Doors', visible: false, locked: true, color: '#00ff00' })); lm.addLayer(makeLayer('layer-3', { name: 'Windows', visible: true, locked: false, color: '#ff0000' })); }); it('should filter by visible', () => { const result = lm.filterLayers({ visible: true }); expect(result.length).toBe(2); }); it('should filter by locked', () => { const result = lm.filterLayers({ locked: true }); expect(result.length).toBe(1); expect(result[0].id).toBe('layer-2'); }); it('should filter by color', () => { const result = lm.filterLayers({ color: '#ff0000' }); expect(result.length).toBe(2); }); it('should filter by name contains (case insensitive)', () => { const result = lm.filterLayers({ nameContains: 'wall' }); expect(result.length).toBe(1); expect(result[0].id).toBe('layer-1'); }); it('should filter by multiple criteria', () => { const result = lm.filterLayers({ visible: true, color: '#ff0000' }); expect(result.length).toBe(2); }); }); describe('getDescendantIds', () => { it('should get all descendant IDs recursively', () => { lm.addLayer(makeLayer('root')); lm.addLayer(makeLayer('child-1', { parentId: 'root' })); lm.addLayer(makeLayer('child-2', { parentId: 'root' })); lm.addLayer(makeLayer('grandchild-1', { parentId: 'child-1' })); lm.addLayer(makeLayer('grandchild-2', { parentId: 'child-1' })); const descendants = lm.getDescendantIds('root'); expect(descendants.length).toBe(4); expect(descendants).toContain('child-1'); expect(descendants).toContain('child-2'); expect(descendants).toContain('grandchild-1'); expect(descendants).toContain('grandchild-2'); }); it('should return empty array for layer with no children', () => { lm.addLayer(makeLayer('lonely')); expect(lm.getDescendantIds('lonely')).toEqual([]); }); }); describe('isLocked', () => { it('should return true for locked layer', () => { lm.addLayer(makeLayer('layer-1', { locked: true })); expect(lm.isLocked('layer-1')).toBe(true); }); it('should return false for unlocked layer', () => { lm.addLayer(makeLayer('layer-1', { locked: false })); expect(lm.isLocked('layer-1')).toBe(false); }); it('should return false for non-existent layer', () => { expect(lm.isLocked('non-existent')).toBe(false); }); }); describe('getLayerColor', () => { it('should return layer color', () => { lm.addLayer(makeLayer('layer-1', { color: '#abcdef' })); expect(lm.getLayerColor('layer-1')).toBe('#abcdef'); }); it('should return undefined for non-existent layer', () => { expect(lm.getLayerColor('non-existent')).toBeUndefined(); }); }); });