tracker.test.ts
1 /** 2 * Tests for window tracker module 3 */ 4 import { describe, it, expect, beforeEach } from 'vitest'; 5 import { WindowTracker, resetWindowTracker, getWindowTracker } from '../../../src/lib/window/tracker'; 6 import type { WindowMapping } from '../../../src/lib/types'; 7 8 describe('WindowTracker', () => { 9 let tracker: WindowTracker; 10 11 beforeEach(() => { 12 resetWindowTracker(); 13 tracker = new WindowTracker(); 14 }); 15 16 describe('setMapping', () => { 17 it('adds a new mapping', () => { 18 const mapping: WindowMapping = { 19 windowId: 1, 20 workspaceId: 'ws-1', 21 isMainWindow: false, 22 isTranscendent: false, 23 }; 24 25 tracker.setMapping(mapping); 26 27 expect(tracker.getWindowMapping(1)).toEqual(mapping); 28 expect(tracker.getWorkspaceWindow('ws-1')).toBe(1); 29 }); 30 31 it('updates existing mapping for same window', () => { 32 tracker.setMapping({ 33 windowId: 1, 34 workspaceId: 'ws-1', 35 isMainWindow: false, 36 isTranscendent: false, 37 }); 38 39 tracker.setMapping({ 40 windowId: 1, 41 workspaceId: 'ws-2', 42 isMainWindow: true, 43 isTranscendent: true, 44 }); 45 46 expect(tracker.getWindowMapping(1)?.workspaceId).toBe('ws-2'); 47 expect(tracker.getWorkspaceWindow('ws-1')).toBeUndefined(); 48 expect(tracker.getWorkspaceWindow('ws-2')).toBe(1); 49 }); 50 }); 51 52 describe('removeWindow', () => { 53 it('removes mapping by window ID', () => { 54 tracker.setMapping({ 55 windowId: 1, 56 workspaceId: 'ws-1', 57 isMainWindow: false, 58 isTranscendent: false, 59 }); 60 61 tracker.removeWindow(1); 62 63 expect(tracker.getWindowMapping(1)).toBeUndefined(); 64 expect(tracker.getWorkspaceWindow('ws-1')).toBeUndefined(); 65 }); 66 67 it('does nothing for unknown window', () => { 68 tracker.removeWindow(999); 69 expect(tracker.getMappings()).toHaveLength(0); 70 }); 71 }); 72 73 describe('removeWorkspace', () => { 74 it('removes mapping by workspace ID', () => { 75 tracker.setMapping({ 76 windowId: 1, 77 workspaceId: 'ws-1', 78 isMainWindow: false, 79 isTranscendent: false, 80 }); 81 82 tracker.removeWorkspace('ws-1'); 83 84 expect(tracker.getWindowMapping(1)).toBeUndefined(); 85 expect(tracker.getWorkspaceWindow('ws-1')).toBeUndefined(); 86 }); 87 }); 88 89 describe('isWindowTracked', () => { 90 it('returns true for tracked window', () => { 91 tracker.setMapping({ 92 windowId: 1, 93 workspaceId: 'ws-1', 94 isMainWindow: false, 95 isTranscendent: false, 96 }); 97 98 expect(tracker.isWindowTracked(1)).toBe(true); 99 }); 100 101 it('returns false for untracked window', () => { 102 expect(tracker.isWindowTracked(999)).toBe(false); 103 }); 104 }); 105 106 describe('hasActiveWindow', () => { 107 it('returns true for workspace with window', () => { 108 tracker.setMapping({ 109 windowId: 1, 110 workspaceId: 'ws-1', 111 isMainWindow: false, 112 isTranscendent: false, 113 }); 114 115 expect(tracker.hasActiveWindow('ws-1')).toBe(true); 116 }); 117 118 it('returns false for workspace without window', () => { 119 expect(tracker.hasActiveWindow('ws-999')).toBe(false); 120 }); 121 }); 122 123 describe('getTranscendentWindows', () => { 124 it('returns only transcendent windows', () => { 125 tracker.setMapping({ 126 windowId: 1, 127 workspaceId: 'ws-1', 128 isMainWindow: false, 129 isTranscendent: true, 130 }); 131 tracker.setMapping({ 132 windowId: 2, 133 workspaceId: 'ws-2', 134 isMainWindow: false, 135 isTranscendent: false, 136 }); 137 tracker.setMapping({ 138 windowId: 3, 139 workspaceId: 'ws-3', 140 isMainWindow: false, 141 isTranscendent: true, 142 }); 143 144 const transcendent = tracker.getTranscendentWindows(); 145 146 expect(transcendent).toHaveLength(2); 147 expect(transcendent).toContain(1); 148 expect(transcendent).toContain(3); 149 expect(transcendent).not.toContain(2); 150 }); 151 }); 152 153 describe('getNonTranscendentWindows', () => { 154 it('returns only non-transcendent windows', () => { 155 tracker.setMapping({ 156 windowId: 1, 157 workspaceId: 'ws-1', 158 isMainWindow: false, 159 isTranscendent: true, 160 }); 161 tracker.setMapping({ 162 windowId: 2, 163 workspaceId: 'ws-2', 164 isMainWindow: false, 165 isTranscendent: false, 166 }); 167 168 const nonTranscendent = tracker.getNonTranscendentWindows(); 169 170 expect(nonTranscendent).toHaveLength(1); 171 expect(nonTranscendent).toContain(2); 172 }); 173 }); 174 175 describe('getMappings', () => { 176 it('returns all mappings', () => { 177 tracker.setMapping({ 178 windowId: 1, 179 workspaceId: 'ws-1', 180 isMainWindow: false, 181 isTranscendent: false, 182 }); 183 tracker.setMapping({ 184 windowId: 2, 185 workspaceId: 'ws-2', 186 isMainWindow: true, 187 isTranscendent: true, 188 }); 189 190 const mappings = tracker.getMappings(); 191 192 expect(mappings).toHaveLength(2); 193 }); 194 }); 195 196 describe('clear', () => { 197 it('removes all mappings', () => { 198 tracker.setMapping({ 199 windowId: 1, 200 workspaceId: 'ws-1', 201 isMainWindow: false, 202 isTranscendent: false, 203 }); 204 tracker.setMapping({ 205 windowId: 2, 206 workspaceId: 'ws-2', 207 isMainWindow: false, 208 isTranscendent: false, 209 }); 210 211 tracker.clear(); 212 213 expect(tracker.getMappings()).toHaveLength(0); 214 expect(tracker.isWindowTracked(1)).toBe(false); 215 expect(tracker.isWindowTracked(2)).toBe(false); 216 }); 217 }); 218 219 describe('subscribe', () => { 220 it('notifies listeners on mapping changes', () => { 221 const listener = vi.fn(); 222 tracker.subscribe(listener); 223 224 tracker.setMapping({ 225 windowId: 1, 226 workspaceId: 'ws-1', 227 isMainWindow: false, 228 isTranscendent: false, 229 }); 230 231 expect(listener).toHaveBeenCalledTimes(1); 232 expect(listener).toHaveBeenCalledWith(tracker.getMappings()); 233 }); 234 235 it('returns unsubscribe function', () => { 236 const listener = vi.fn(); 237 const unsubscribe = tracker.subscribe(listener); 238 239 tracker.setMapping({ 240 windowId: 1, 241 workspaceId: 'ws-1', 242 isMainWindow: false, 243 isTranscendent: false, 244 }); 245 246 expect(listener).toHaveBeenCalledTimes(1); 247 248 unsubscribe(); 249 250 tracker.setMapping({ 251 windowId: 2, 252 workspaceId: 'ws-2', 253 isMainWindow: false, 254 isTranscendent: false, 255 }); 256 257 expect(listener).toHaveBeenCalledTimes(1); 258 }); 259 }); 260 }); 261 262 describe('getWindowTracker singleton', () => { 263 beforeEach(() => { 264 resetWindowTracker(); 265 }); 266 267 it('returns the same instance', () => { 268 const tracker1 = getWindowTracker(); 269 const tracker2 = getWindowTracker(); 270 271 expect(tracker1).toBe(tracker2); 272 }); 273 274 it('resets singleton on resetWindowTracker', () => { 275 const tracker1 = getWindowTracker(); 276 tracker1.setMapping({ 277 windowId: 1, 278 workspaceId: 'ws-1', 279 isMainWindow: false, 280 isTranscendent: false, 281 }); 282 283 resetWindowTracker(); 284 285 const tracker2 = getWindowTracker(); 286 expect(tracker2.getMappings()).toHaveLength(0); 287 }); 288 });