592 lines · 18.3 KB · TypeScript
RawDownload
/** * Tab slice unit tests. * Tests tab state management including deduplication, forceNewTab, scroll position, * and the unified navigation request model. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { installMockElectronAPI, type MockElectronAPI } from '../../mocks/electronAPI'; import { createTestStore, type TestStore } from './storeTestUtils'; import type { TabNavigationRequest } from '../../../src/renderer/types/tabs'; describe('tabSlice', () => {  let store: TestStore;  let mockAPI: MockElectronAPI;   beforeEach(() => {    vi.useFakeTimers();    mockAPI = installMockElectronAPI();    store = createTestStore();     // Mock crypto.randomUUID for predictable tab IDs    let uuidCounter = 0;    vi.stubGlobal('crypto', {      randomUUID: () => `test-uuid-${++uuidCounter}`,    });  });   afterEach(() => {    vi.useRealTimers();    vi.restoreAllMocks();  });   describe('openTab', () => {    describe('deduplication', () => {      it('should focus existing tab when opening same session', () => {        // Open initial session tab        store.getState().openTab({          type: 'session',          sessionId: 'session-1',          projectId: 'project-1',          label: 'First Session',        });         const initialTabId = store.getState().activeTabId;        expect(store.getState().openTabs).toHaveLength(1);         // Open another tab        store.getState().openTab({          type: 'session',          sessionId: 'session-2',          projectId: 'project-1',          label: 'Second Session',        });         expect(store.getState().openTabs).toHaveLength(2);        expect(store.getState().activeTabId).not.toBe(initialTabId);         // Try to open session-1 again - should deduplicate        store.getState().openTab({          type: 'session',          sessionId: 'session-1',          projectId: 'project-1',          label: 'First Session Again',        });         expect(store.getState().openTabs).toHaveLength(2);        expect(store.getState().activeTabId).toBe(initialTabId);      });       it('should bypass deduplication when forceNewTab is true', () => {        // Open initial session tab        store.getState().openTab({          type: 'session',          sessionId: 'session-1',          projectId: 'project-1',          label: 'First Session',        });         const initialTabId = store.getState().activeTabId;        expect(store.getState().openTabs).toHaveLength(1);         // Open same session with forceNewTab        store.getState().openTab(          {            type: 'session',            sessionId: 'session-1',            projectId: 'project-1',            label: 'First Session (New Tab)',          },          { forceNewTab: true }        );         // Should have 2 tabs now, both for the same session        expect(store.getState().openTabs).toHaveLength(2);        expect(store.getState().activeTabId).not.toBe(initialTabId);         // Both tabs should have the same sessionId        const sessionTabs = store.getState().openTabs.filter((t) => t.sessionId === 'session-1');        expect(sessionTabs).toHaveLength(2);      });       it('should not deduplicate dashboard tabs', () => {        store.getState().openDashboard();        store.getState().openDashboard();         expect(store.getState().openTabs).toHaveLength(2);        expect(store.getState().openTabs.filter((t) => t.type === 'dashboard')).toHaveLength(2);      });    });     describe('dashboard replacement', () => {      it('should replace active dashboard tab when opening session', () => {        store.getState().openDashboard();        const dashboardTabId = store.getState().activeTabId;         store.getState().openTab({          type: 'session',          sessionId: 'session-1',          projectId: 'project-1',          label: 'Session 1',        });         expect(store.getState().openTabs).toHaveLength(1);        // Tab should keep same ID (position preserved)        expect(store.getState().activeTabId).toBe(dashboardTabId);        // But now it's a session tab        expect(store.getState().openTabs[0].type).toBe('session');        expect(store.getState().openTabs[0].sessionId).toBe('session-1');      });    });     describe('label truncation', () => {      it('should truncate labels longer than 50 characters', () => {        const longLabel = 'A'.repeat(60);         store.getState().openTab({          type: 'session',          sessionId: 'session-1',          projectId: 'project-1',          label: longLabel,        });         const tab = store.getState().openTabs[0];        expect(tab.label).toHaveLength(50);        expect(tab.label.endsWith('…')).toBe(true);      });    });  });   describe('closeTab', () => {    it('should focus adjacent tab when closing active tab', () => {      // Open 3 tabs      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Tab 1',      });       store.getState().openTab({        type: 'session',        sessionId: 'session-2',        projectId: 'project-1',        label: 'Tab 2',      });      const tab2Id = store.getState().activeTabId;       store.getState().openTab({        type: 'session',        sessionId: 'session-3',        projectId: 'project-1',        label: 'Tab 3',      });      const tab3Id = store.getState().activeTabId;       // Close tab 3 (active tab)      store.getState().closeTab(tab3Id!);       // Should focus tab 2 (previous tab)      expect(store.getState().openTabs).toHaveLength(2);      expect(store.getState().activeTabId).toBe(tab2Id);    });     it('should reset state when all tabs closed', () => {      // Setup some state      store.setState({        selectedProjectId: 'project-1',        selectedSessionId: 'session-1',      });       store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Tab 1',      });      const tabId = store.getState().activeTabId;       store.getState().closeTab(tabId!);       expect(store.getState().openTabs).toHaveLength(0);      expect(store.getState().activeTabId).toBeNull();      expect(store.getState().selectedProjectId).toBeNull();      expect(store.getState().selectedSessionId).toBeNull();    });  });   describe('setActiveTab', () => {    it('should update activeTabId', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });      const tab1Id = store.getState().activeTabId;       store.getState().openTab({        type: 'session',        sessionId: 'session-2',        projectId: 'project-1',        label: 'Session 2',      });       // Switch back to first tab      store.getState().setActiveTab(tab1Id!);       expect(store.getState().activeTabId).toBe(tab1Id);    });     it('should preserve sidebar state for non-session tabs', () => {      // Setup initial state with projects data so setActiveTab can find the project      store.setState({        selectedProjectId: 'project-1',        selectedSessionId: 'session-1',        projects: [          { id: 'project-1', name: 'Project 1', path: '/path/1', sessions: ['session-1'] },          { id: 'project-2', name: 'Project 2', path: '/path/2', sessions: ['session-2'] },        ] as never[],      });       // Open session-2 tab first (this doesn't call setActiveTab, just sets activeTabId)      store.getState().openTab({        type: 'session',        sessionId: 'session-2',        projectId: 'project-2',        label: 'Session 2',      });      const sessionTabId = store.getState().activeTabId;       // Manually call setActiveTab to sync sidebar state (simulating user click)      store.getState().setActiveTab(sessionTabId!);      expect(store.getState().selectedProjectId).toBe('project-2');       // Open dashboard tab      store.getState().openDashboard();      const dashboardTabId = store.getState().activeTabId;       // Switch to dashboard (should preserve sidebar state)      store.getState().setActiveTab(dashboardTabId!);       expect(store.getState().activeTabId).toBe(dashboardTabId);      // Sidebar state should be preserved (not cleared) when switching to dashboard      expect(store.getState().selectedProjectId).toBe('project-2');    });  });   describe('saveTabScrollPosition', () => {    it('should save scroll position for a tab', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });      const tabId = store.getState().activeTabId!;       // Initially undefined      expect(store.getState().openTabs[0].savedScrollTop).toBeUndefined();       // Save scroll position      store.getState().saveTabScrollPosition(tabId, 500);       expect(store.getState().openTabs[0].savedScrollTop).toBe(500);    });     it('should only update the specified tab', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });      const tab1Id = store.getState().activeTabId!;       store.getState().openTab({        type: 'session',        sessionId: 'session-2',        projectId: 'project-1',        label: 'Session 2',      });       // Save scroll position for tab 1      store.getState().saveTabScrollPosition(tab1Id, 300);       // Tab 1 should have scroll position, tab 2 should not      const tab1 = store.getState().openTabs.find((t) => t.id === tab1Id);      const tab2 = store.getState().openTabs.find((t) => t.id !== tab1Id);       expect(tab1?.savedScrollTop).toBe(300);      expect(tab2?.savedScrollTop).toBeUndefined();    });  });   describe('setTabContextPanelVisible', () => {    it('should set context panel visibility for a tab', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });      const tabId = store.getState().activeTabId!;       // Initially undefined      expect(store.getState().openTabs[0].showContextPanel).toBeUndefined();       // Set to true      store.getState().setTabContextPanelVisible(tabId, true);      expect(store.getState().openTabs[0].showContextPanel).toBe(true);       // Set to false      store.getState().setTabContextPanelVisible(tabId, false);      expect(store.getState().openTabs[0].showContextPanel).toBe(false);    });     it('should only update the specified tab', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });      const tab1Id = store.getState().activeTabId!;       store.getState().openTab({        type: 'session',        sessionId: 'session-2',        projectId: 'project-1',        label: 'Session 2',      });       // Set context panel visible for tab 1      store.getState().setTabContextPanelVisible(tab1Id, true);       // Tab 1 should have context panel visible, tab 2 should not      const tab1 = store.getState().openTabs.find((t) => t.id === tab1Id);      const tab2 = store.getState().openTabs.find((t) => t.id !== tab1Id);       expect(tab1?.showContextPanel).toBe(true);      expect(tab2?.showContextPanel).toBeUndefined();    });  });   describe('enqueueTabNavigation', () => {    it('should set pendingNavigation on the tab', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });       const tabId = store.getState().activeTabId!;      const request: TabNavigationRequest = {        id: 'nav-1',        kind: 'error',        source: 'notification',        highlight: 'red',        payload: {          errorId: 'error-1',          errorTimestamp: 12345,          toolUseId: 'tool-1',          lineNumber: 42,        },      };       store.getState().enqueueTabNavigation(tabId, request);       const tab = store.getState().openTabs[0];      expect(tab.pendingNavigation).toEqual(request);    });     it('should replace existing pendingNavigation with new request', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });       const tabId = store.getState().activeTabId!;      const request1: TabNavigationRequest = {        id: 'nav-1',        kind: 'error',        source: 'notification',        highlight: 'red',        payload: { errorId: 'e1', errorTimestamp: 100 },      };      const request2: TabNavigationRequest = {        id: 'nav-2',        kind: 'error',        source: 'notification',        highlight: 'red',        payload: { errorId: 'e2', errorTimestamp: 200 },      };       store.getState().enqueueTabNavigation(tabId, request1);      store.getState().enqueueTabNavigation(tabId, request2);       const tab = store.getState().openTabs[0];      expect(tab.pendingNavigation?.id).toBe('nav-2');    });     it('should only update the specified tab', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });      const tab1Id = store.getState().activeTabId!;       store.getState().openTab({        type: 'session',        sessionId: 'session-2',        projectId: 'project-1',        label: 'Session 2',      });       const request: TabNavigationRequest = {        id: 'nav-1',        kind: 'search',        source: 'commandPalette',        highlight: 'yellow',        payload: { query: 'test', messageTimestamp: 1234, matchedText: 'match' },      };       store.getState().enqueueTabNavigation(tab1Id, request);       const tab1 = store.getState().openTabs.find((t) => t.id === tab1Id);      const tab2 = store.getState().openTabs.find((t) => t.id !== tab1Id);      expect(tab1?.pendingNavigation).toEqual(request);      expect(tab2?.pendingNavigation).toBeUndefined();    });  });   describe('consumeTabNavigation', () => {    it('should clear pendingNavigation and set lastConsumedNavigationId', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });       const tabId = store.getState().activeTabId!;      const request: TabNavigationRequest = {        id: 'nav-1',        kind: 'error',        source: 'notification',        highlight: 'red',        payload: { errorId: 'error-1', errorTimestamp: 12345 },      };       store.getState().enqueueTabNavigation(tabId, request);      expect(store.getState().openTabs[0].pendingNavigation).toBeDefined();       store.getState().consumeTabNavigation(tabId, 'nav-1');       const tab = store.getState().openTabs[0];      expect(tab.pendingNavigation).toBeUndefined();      expect(tab.lastConsumedNavigationId).toBe('nav-1');    });     it('should not clear if requestId does not match', () => {      store.getState().openTab({        type: 'session',        sessionId: 'session-1',        projectId: 'project-1',        label: 'Session 1',      });       const tabId = store.getState().activeTabId!;      const request: TabNavigationRequest = {        id: 'nav-1',        kind: 'error',

Lines 1–500 of 592.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t