groups.test.ts
1 /** 2 * Tests for workspace groups - organizational subgroups within parent workspaces 3 * Following TDD: Write failing tests first, then implement 4 */ 5 6 import { describe, it, expect } from 'vitest'; 7 import { validateWorkspaceGroups } from '../../../src/lib/workspace/validation'; 8 import type { 9 WorkspaceGroup, 10 ParentWorkspace, 11 ChildWorkspace, 12 } from '../../../src/lib/types'; 13 14 // Helper to create valid metadata 15 const createMetadata = () => ({ 16 created: new Date().toISOString(), 17 lastModified: new Date().toISOString(), 18 lastAccessed: new Date().toISOString(), 19 switchCount: 0, 20 totalTimeActive: 0, 21 }); 22 23 // Helper to create a parent workspace with optional groups 24 function createParent( 25 id: string, 26 childIds: string[] = [], 27 groups?: WorkspaceGroup[] 28 ): ParentWorkspace { 29 return { 30 id, 31 name: `Parent ${id}`, 32 type: 'parent', 33 parentId: null, 34 isTranscendent: false, 35 openInContextSwitch: true, 36 children: childIds, 37 groups, 38 metadata: createMetadata(), 39 }; 40 } 41 42 // Helper to create a child workspace with optional groupId 43 function createChild( 44 id: string, 45 parentId: string, 46 groupId?: string | null 47 ): ChildWorkspace { 48 return { 49 id, 50 name: `Child ${id}`, 51 type: 'child', 52 parentId, 53 isTranscendent: false, 54 openInContextSwitch: true, 55 tabs: [], 56 groupId, 57 metadata: createMetadata(), 58 }; 59 } 60 61 describe('WorkspaceGroup value object', () => { 62 it('should have required id and name fields', () => { 63 const group: WorkspaceGroup = { 64 id: 'group-1', 65 name: 'My Group', 66 }; 67 68 expect(group.id).toBe('group-1'); 69 expect(group.name).toBe('My Group'); 70 }); 71 72 it('should default collapsed to undefined (treated as false)', () => { 73 const group: WorkspaceGroup = { 74 id: 'group-1', 75 name: 'My Group', 76 }; 77 78 expect(group.collapsed).toBeUndefined(); 79 }); 80 81 it('should allow optional collapsed field', () => { 82 const collapsedGroup: WorkspaceGroup = { 83 id: 'group-1', 84 name: 'My Group', 85 collapsed: true, 86 }; 87 88 const expandedGroup: WorkspaceGroup = { 89 id: 'group-2', 90 name: 'Another Group', 91 collapsed: false, 92 }; 93 94 expect(collapsedGroup.collapsed).toBe(true); 95 expect(expandedGroup.collapsed).toBe(false); 96 }); 97 98 it('should allow optional color', () => { 99 const coloredGroup: WorkspaceGroup = { 100 id: 'group-1', 101 name: 'My Group', 102 color: '#ff5500', 103 }; 104 105 expect(coloredGroup.color).toBe('#ff5500'); 106 }); 107 }); 108 109 describe('ParentWorkspace with groups', () => { 110 it('should accept optional groups array', () => { 111 // Parent without groups is valid (backwards compatible) 112 const parentWithoutGroups = createParent('parent-1', ['child-1']); 113 expect(parentWithoutGroups.groups).toBeUndefined(); 114 115 // Parent with groups 116 const parentWithGroups = createParent('parent-2', ['child-1', 'child-2'], [ 117 { id: 'group-1', name: 'Group A' }, 118 { id: 'group-2', name: 'Group B' }, 119 ]); 120 expect(parentWithGroups.groups).toHaveLength(2); 121 expect(parentWithGroups.groups![0].name).toBe('Group A'); 122 }); 123 124 it('should accept empty groups array', () => { 125 const parent = createParent('parent-1', [], []); 126 expect(parent.groups).toEqual([]); 127 }); 128 }); 129 130 describe('ChildWorkspace with groupId', () => { 131 it('should accept optional groupId field', () => { 132 // Child without groupId is valid (ungrouped) 133 const childWithoutGroup = createChild('child-1', 'parent-1'); 134 expect(childWithoutGroup.groupId).toBeUndefined(); 135 136 // Child with groupId 137 const childWithGroup = createChild('child-2', 'parent-1', 'group-1'); 138 expect(childWithGroup.groupId).toBe('group-1'); 139 }); 140 141 it('should allow null groupId for explicitly ungrouped children', () => { 142 const child = createChild('child-1', 'parent-1', null); 143 expect(child.groupId).toBeNull(); 144 }); 145 }); 146 147 describe('validateWorkspaceGroups', () => { 148 it('should pass when child groupId references existing group in parent', () => { 149 const parent = createParent('parent-1', ['child-1'], [ 150 { id: 'group-1', name: 'Group A' }, 151 ]); 152 const child = createChild('child-1', 'parent-1', 'group-1'); 153 154 const result = validateWorkspaceGroups(parent, [child]); 155 expect(result.valid).toBe(true); 156 expect(result.errors).toHaveLength(0); 157 }); 158 159 it('should fail when child groupId references non-existent group', () => { 160 const parent = createParent('parent-1', ['child-1'], [ 161 { id: 'group-1', name: 'Group A' }, 162 ]); 163 const child = createChild('child-1', 'parent-1', 'non-existent-group'); 164 165 const result = validateWorkspaceGroups(parent, [child]); 166 expect(result.valid).toBe(false); 167 expect(result.errors.some(e => e.message.includes('non-existent group'))).toBe(true); 168 expect(result.errors[0].workspaceId).toBe('child-1'); 169 }); 170 171 it('should pass when child has no groupId (ungrouped)', () => { 172 const parent = createParent('parent-1', ['child-1'], [ 173 { id: 'group-1', name: 'Group A' }, 174 ]); 175 const childWithoutGroup = createChild('child-1', 'parent-1'); 176 177 const result = validateWorkspaceGroups(parent, [childWithoutGroup]); 178 expect(result.valid).toBe(true); 179 }); 180 181 it('should pass when child has null groupId (explicitly ungrouped)', () => { 182 const parent = createParent('parent-1', ['child-1'], [ 183 { id: 'group-1', name: 'Group A' }, 184 ]); 185 const child = createChild('child-1', 'parent-1', null); 186 187 const result = validateWorkspaceGroups(parent, [child]); 188 expect(result.valid).toBe(true); 189 }); 190 191 it('should fail when groups array has duplicate IDs', () => { 192 const parent = createParent('parent-1', [], [ 193 { id: 'group-1', name: 'Group A' }, 194 { id: 'group-1', name: 'Group B' }, // Duplicate ID! 195 ]); 196 197 const result = validateWorkspaceGroups(parent, []); 198 expect(result.valid).toBe(false); 199 expect(result.errors.some(e => e.message.includes('Duplicate group ID'))).toBe(true); 200 expect(result.errors[0].workspaceId).toBe('parent-1'); 201 }); 202 203 it('should pass for parent without groups (backwards compatible)', () => { 204 const parent = createParent('parent-1', ['child-1']); 205 const child = createChild('child-1', 'parent-1'); 206 207 const result = validateWorkspaceGroups(parent, [child]); 208 expect(result.valid).toBe(true); 209 }); 210 211 it('should validate multiple children correctly', () => { 212 const parent = createParent('parent-1', ['child-1', 'child-2', 'child-3'], [ 213 { id: 'group-1', name: 'Group A' }, 214 { id: 'group-2', name: 'Group B' }, 215 ]); 216 const child1 = createChild('child-1', 'parent-1', 'group-1'); 217 const child2 = createChild('child-2', 'parent-1', 'group-2'); 218 const child3 = createChild('child-3', 'parent-1'); // Ungrouped 219 220 const result = validateWorkspaceGroups(parent, [child1, child2, child3]); 221 expect(result.valid).toBe(true); 222 }); 223 224 it('should report multiple errors when multiple children reference invalid groups', () => { 225 const parent = createParent('parent-1', ['child-1', 'child-2'], [ 226 { id: 'group-1', name: 'Group A' }, 227 ]); 228 const child1 = createChild('child-1', 'parent-1', 'invalid-1'); 229 const child2 = createChild('child-2', 'parent-1', 'invalid-2'); 230 231 const result = validateWorkspaceGroups(parent, [child1, child2]); 232 expect(result.valid).toBe(false); 233 expect(result.errors).toHaveLength(2); 234 }); 235 });