agent-ecosystem/test/renderer/store/tabUISlice.test.ts

375 lines
14 KiB
TypeScript

/**
* 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);
});
});
});