/ tests / unit / window / tracker.test.ts
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  });