347 lines
12 KiB
TypeScript
347 lines
12 KiB
TypeScript
/**
|
||
* 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> = {}): 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();
|
||
});
|
||
});
|
||
});
|