/** * TabUI slice unit tests. * Tests per-tab UI state isolation (expansion states, context panel, scroll position). */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { installMockElectronAPI, type MockElectronAPI } from '../../mocks/electronAPI'; import { createTestStore, type TestStore } from './storeTestUtils'; describe('tabUISlice', () => { 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('initTabUIState', () => { it('should initialize UI state for a new tab', () => { expect(store.getState().tabUIStates.size).toBe(0); store.getState().initTabUIState('tab-1'); expect(store.getState().tabUIStates.size).toBe(1); expect(store.getState().tabUIStates.has('tab-1')).toBe(true); const tabState = store.getState().tabUIStates.get('tab-1'); expect(tabState?.expandedAIGroupIds.size).toBe(0); expect(tabState?.expandedDisplayItemIds.size).toBe(0); expect(tabState?.expandedSubagentTraceIds.size).toBe(0); expect(tabState?.showContextPanel).toBe(false); expect(tabState?.savedScrollTop).toBeUndefined(); }); it('should not reinitialize existing tab state', () => { store.getState().initTabUIState('tab-1'); store.getState().toggleAIGroupExpansionForTab('tab-1', 'group-1'); // Try to reinitialize store.getState().initTabUIState('tab-1'); // Should still have the expanded group expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-1')).toBe(true); }); }); describe('cleanupTabUIState', () => { it('should remove UI state for a tab', () => { store.getState().initTabUIState('tab-1'); store.getState().initTabUIState('tab-2'); expect(store.getState().tabUIStates.size).toBe(2); store.getState().cleanupTabUIState('tab-1'); expect(store.getState().tabUIStates.size).toBe(1); expect(store.getState().tabUIStates.has('tab-1')).toBe(false); expect(store.getState().tabUIStates.has('tab-2')).toBe(true); }); it('should do nothing if tab does not exist', () => { store.getState().initTabUIState('tab-1'); store.getState().cleanupTabUIState('nonexistent'); expect(store.getState().tabUIStates.size).toBe(1); }); }); describe('AI Group expansion - per-tab isolation', () => { it('should toggle AI group expansion for specific tab', () => { store.getState().initTabUIState('tab-1'); expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-1')).toBe(false); store.getState().toggleAIGroupExpansionForTab('tab-1', 'group-1'); expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-1')).toBe(true); store.getState().toggleAIGroupExpansionForTab('tab-1', 'group-1'); expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-1')).toBe(false); }); it('should isolate AI group expansion between tabs', () => { store.getState().initTabUIState('tab-1'); store.getState().initTabUIState('tab-2'); // Expand group-1 in tab-1 only store.getState().toggleAIGroupExpansionForTab('tab-1', 'group-1'); // tab-1 should have it expanded, tab-2 should not expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-1')).toBe(true); expect(store.getState().isAIGroupExpandedForTab('tab-2', 'group-1')).toBe(false); // Expand different group in tab-2 store.getState().toggleAIGroupExpansionForTab('tab-2', 'group-2'); // Each tab has its own expansion state expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-1')).toBe(true); expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-2')).toBe(false); expect(store.getState().isAIGroupExpandedForTab('tab-2', 'group-1')).toBe(false); expect(store.getState().isAIGroupExpandedForTab('tab-2', 'group-2')).toBe(true); }); it('should expand AI group programmatically', () => { store.getState().initTabUIState('tab-1'); store.getState().expandAIGroupForTab('tab-1', 'group-1'); expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-1')).toBe(true); // Calling expand again should not change state (idempotent) store.getState().expandAIGroupForTab('tab-1', 'group-1'); expect(store.getState().isAIGroupExpandedForTab('tab-1', 'group-1')).toBe(true); }); }); describe('Display item expansion - per-tab isolation', () => { it('should toggle display item expansion within AI group', () => { store.getState().initTabUIState('tab-1'); const items = store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-1'); expect(items.size).toBe(0); store.getState().toggleDisplayItemExpansionForTab('tab-1', 'group-1', 'item-1'); const updatedItems = store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-1'); expect(updatedItems.has('item-1')).toBe(true); store.getState().toggleDisplayItemExpansionForTab('tab-1', 'group-1', 'item-1'); const finalItems = store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-1'); expect(finalItems.has('item-1')).toBe(false); }); it('should isolate display item expansion between tabs', () => { store.getState().initTabUIState('tab-1'); store.getState().initTabUIState('tab-2'); // Expand item in tab-1 store.getState().toggleDisplayItemExpansionForTab('tab-1', 'group-1', 'item-1'); // tab-1 should have it, tab-2 should not expect( store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-1').has('item-1') ).toBe(true); expect( store.getState().getExpandedDisplayItemIdsForTab('tab-2', 'group-1').has('item-1') ).toBe(false); }); it('should isolate display items by AI group within same tab', () => { store.getState().initTabUIState('tab-1'); store.getState().toggleDisplayItemExpansionForTab('tab-1', 'group-1', 'item-1'); store.getState().toggleDisplayItemExpansionForTab('tab-1', 'group-2', 'item-2'); expect( store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-1').has('item-1') ).toBe(true); expect( store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-1').has('item-2') ).toBe(false); expect( store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-2').has('item-1') ).toBe(false); expect( store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-2').has('item-2') ).toBe(true); }); it('should expand display item programmatically', () => { store.getState().initTabUIState('tab-1'); store.getState().expandDisplayItemForTab('tab-1', 'group-1', 'item-1'); expect( store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-1').has('item-1') ).toBe(true); // Calling expand again should not change state (idempotent) store.getState().expandDisplayItemForTab('tab-1', 'group-1', 'item-1'); expect( store.getState().getExpandedDisplayItemIdsForTab('tab-1', 'group-1').has('item-1') ).toBe(true); }); }); describe('Subagent trace expansion - per-tab isolation', () => { it('should toggle subagent trace expansion', () => { store.getState().initTabUIState('tab-1'); expect(store.getState().isSubagentTraceExpandedForTab('tab-1', 'subagent-1')).toBe(false); store.getState().toggleSubagentTraceExpansionForTab('tab-1', 'subagent-1'); expect(store.getState().isSubagentTraceExpandedForTab('tab-1', 'subagent-1')).toBe(true); store.getState().toggleSubagentTraceExpansionForTab('tab-1', 'subagent-1'); expect(store.getState().isSubagentTraceExpandedForTab('tab-1', 'subagent-1')).toBe(false); }); it('should isolate subagent trace expansion between tabs', () => { store.getState().initTabUIState('tab-1'); store.getState().initTabUIState('tab-2'); store.getState().toggleSubagentTraceExpansionForTab('tab-1', 'subagent-1'); expect(store.getState().isSubagentTraceExpandedForTab('tab-1', 'subagent-1')).toBe(true); expect(store.getState().isSubagentTraceExpandedForTab('tab-2', 'subagent-1')).toBe(false); }); }); describe('Context panel visibility - per-tab isolation', () => { it('should set context panel visibility', () => { store.getState().initTabUIState('tab-1'); expect(store.getState().isContextPanelVisibleForTab('tab-1')).toBe(false); store.getState().setContextPanelVisibleForTab('tab-1', true); expect(store.getState().isContextPanelVisibleForTab('tab-1')).toBe(true); store.getState().setContextPanelVisibleForTab('tab-1', false); expect(store.getState().isContextPanelVisibleForTab('tab-1')).toBe(false); }); it('should isolate context panel visibility between tabs', () => { store.getState().initTabUIState('tab-1'); store.getState().initTabUIState('tab-2'); store.getState().setContextPanelVisibleForTab('tab-1', true); expect(store.getState().isContextPanelVisibleForTab('tab-1')).toBe(true); expect(store.getState().isContextPanelVisibleForTab('tab-2')).toBe(false); }); }); describe('Scroll position - per-tab isolation', () => { it('should save and retrieve scroll position', () => { store.getState().initTabUIState('tab-1'); expect(store.getState().getScrollPositionForTab('tab-1')).toBeUndefined(); store.getState().saveScrollPositionForTab('tab-1', 500); expect(store.getState().getScrollPositionForTab('tab-1')).toBe(500); store.getState().saveScrollPositionForTab('tab-1', 1000); expect(store.getState().getScrollPositionForTab('tab-1')).toBe(1000); }); it('should isolate scroll positions between tabs', () => { store.getState().initTabUIState('tab-1'); store.getState().initTabUIState('tab-2'); store.getState().saveScrollPositionForTab('tab-1', 100); store.getState().saveScrollPositionForTab('tab-2', 200); expect(store.getState().getScrollPositionForTab('tab-1')).toBe(100); expect(store.getState().getScrollPositionForTab('tab-2')).toBe(200); }); }); describe('Integration with tab lifecycle', () => { it('should handle full tab lifecycle', () => { // Simulate opening a tab store.getState().openTab({ type: 'session', sessionId: 'session-1', projectId: 'project-1', label: 'Session 1', }); const tabId = store.getState().activeTabId!; // Initialize UI state store.getState().initTabUIState(tabId); // Set some UI state store.getState().toggleAIGroupExpansionForTab(tabId, 'group-1'); store.getState().setContextPanelVisibleForTab(tabId, true); store.getState().saveScrollPositionForTab(tabId, 300); // Verify state expect(store.getState().isAIGroupExpandedForTab(tabId, 'group-1')).toBe(true); expect(store.getState().isContextPanelVisibleForTab(tabId)).toBe(true); expect(store.getState().getScrollPositionForTab(tabId)).toBe(300); // Close tab (should cleanup UI state) store.getState().closeTab(tabId); // UI state should be cleaned up expect(store.getState().tabUIStates.has(tabId)).toBe(false); }); it('should maintain separate state for two tabs with same session (forceNewTab)', () => { // Open first tab store.getState().openTab({ type: 'session', sessionId: 'session-1', projectId: 'project-1', label: 'Session 1', }); const tab1Id = store.getState().activeTabId!; store.getState().initTabUIState(tab1Id); // Open second tab with same session (forceNewTab) store.getState().openTab( { type: 'session', sessionId: 'session-1', projectId: 'project-1', label: 'Session 1 (Copy)', }, { forceNewTab: true } ); const tab2Id = store.getState().activeTabId!; store.getState().initTabUIState(tab2Id); // Both tabs should have same session expect(store.getState().openTabs.filter((t) => t.sessionId === 'session-1')).toHaveLength(2); // Set different states for each tab store.getState().toggleAIGroupExpansionForTab(tab1Id, 'group-1'); store.getState().toggleAIGroupExpansionForTab(tab2Id, 'group-2'); store.getState().setContextPanelVisibleForTab(tab1Id, true); store.getState().saveScrollPositionForTab(tab1Id, 100); store.getState().saveScrollPositionForTab(tab2Id, 500); // Verify states are isolated expect(store.getState().isAIGroupExpandedForTab(tab1Id, 'group-1')).toBe(true); expect(store.getState().isAIGroupExpandedForTab(tab1Id, 'group-2')).toBe(false); expect(store.getState().isAIGroupExpandedForTab(tab2Id, 'group-1')).toBe(false); expect(store.getState().isAIGroupExpandedForTab(tab2Id, 'group-2')).toBe(true); expect(store.getState().isContextPanelVisibleForTab(tab1Id)).toBe(true); expect(store.getState().isContextPanelVisibleForTab(tab2Id)).toBe(false); expect(store.getState().getScrollPositionForTab(tab1Id)).toBe(100); expect(store.getState().getScrollPositionForTab(tab2Id)).toBe(500); }); }); describe('Edge cases', () => { it('should return false/empty for uninitialized tab', () => { // No initialization expect(store.getState().isAIGroupExpandedForTab('nonexistent', 'group-1')).toBe(false); expect(store.getState().getExpandedDisplayItemIdsForTab('nonexistent', 'group-1').size).toBe( 0 ); expect(store.getState().isSubagentTraceExpandedForTab('nonexistent', 'subagent-1')).toBe( false ); expect(store.getState().isContextPanelVisibleForTab('nonexistent')).toBe(false); expect(store.getState().getScrollPositionForTab('nonexistent')).toBeUndefined(); }); it('should auto-create tab state when toggling (lazy initialization)', () => { // Toggle without explicit init store.getState().toggleAIGroupExpansionForTab('lazy-tab', 'group-1'); // Should have created the state expect(store.getState().tabUIStates.has('lazy-tab')).toBe(true); expect(store.getState().isAIGroupExpandedForTab('lazy-tab', 'group-1')).toBe(true); }); }); });