From 8e9aae9e57f5c81fe6bd31a5ca532aeb95373ca0 Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Mon, 2 Feb 2026 04:04:03 -0600 Subject: [PATCH] MAESTRO: Add unified tabs drag-and-drop tests Add 12 comprehensive tests for unified tabs drag-and-drop reordering: - AI tab to file tab drag-and-drop calls onUnifiedTabReorder - File tab to AI tab drag-and-drop calls onUnifiedTabReorder - File tab to file tab drag-and-drop works correctly - Dropping on same tab does not trigger reorder - Drag over visual feedback (ring-2 class) on target tab - Falls back to legacy onTabReorder when unifiedTabs not provided - Move to First/Last shown for file tabs not at edges - Move to First hidden for first tab - Move to Last hidden for last tab - Move to First click calls onUnifiedTabReorder - Move to Last click calls onUnifiedTabReorder - Middle-click closes file tabs --- .../renderer/components/TabBar.test.tsx | 470 ++++++++++++++++++ 1 file changed, 470 insertions(+) diff --git a/src/__tests__/renderer/components/TabBar.test.tsx b/src/__tests__/renderer/components/TabBar.test.tsx index adc133cb..56c23518 100644 --- a/src/__tests__/renderer/components/TabBar.test.tsx +++ b/src/__tests__/renderer/components/TabBar.test.tsx @@ -3076,3 +3076,473 @@ describe('FileTab overlay menu', () => { vi.useRealTimers(); }); }); + +describe('Unified tabs drag and drop', () => { + const mockOnUnifiedTabReorder = vi.fn(); + const mockOnTabReorder = vi.fn(); + const mockOnFileTabSelect = vi.fn(); + const mockOnFileTabClose = vi.fn(); + + beforeEach(() => { + vi.useFakeTimers(); + vi.clearAllMocks(); + Element.prototype.scrollTo = vi.fn(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + const aiTab1 = createTab({ id: 'ai-tab-1', name: 'AI Tab 1', agentSessionId: 'sess-1' }); + const aiTab2 = createTab({ id: 'ai-tab-2', name: 'AI Tab 2', agentSessionId: 'sess-2' }); + const aiTabs: AITab[] = [aiTab1, aiTab2]; + + const fileTab1: FilePreviewTab = { + id: 'file-tab-1', + path: '/path/to/file1.ts', + name: 'file1', + extension: '.ts', + scrollTop: 0, + searchQuery: '', + editMode: false, + editContent: undefined, + createdAt: Date.now(), + }; + + const fileTab2: FilePreviewTab = { + id: 'file-tab-2', + path: '/path/to/file2.md', + name: 'file2', + extension: '.md', + scrollTop: 0, + searchQuery: '', + editMode: false, + editContent: undefined, + createdAt: Date.now() + 1, + }; + + // Unified tabs: AI, File, AI, File + const unifiedTabs = [ + { type: 'ai' as const, id: 'ai-tab-1', data: aiTab1 }, + { type: 'file' as const, id: 'file-tab-1', data: fileTab1 }, + { type: 'ai' as const, id: 'ai-tab-2', data: aiTab2 }, + { type: 'file' as const, id: 'file-tab-2', data: fileTab2 }, + ]; + + it('drags AI tab to file tab position and calls onUnifiedTabReorder', () => { + render( + + ); + + const aiTabElement = screen.getByText('AI Tab 1').closest('[data-tab-id]')!; + const fileTabElement = screen.getByText('file1').closest('[data-tab-id]')!; + + // Start dragging ai-tab-1 + fireEvent.dragStart(aiTabElement, { + dataTransfer: { + effectAllowed: '', + setData: vi.fn(), + getData: vi.fn().mockReturnValue('ai-tab-1'), + }, + }); + + // Drop on file-tab-1 + fireEvent.drop(fileTabElement, { + dataTransfer: { + getData: vi.fn().mockReturnValue('ai-tab-1'), + }, + }); + + // Should call onUnifiedTabReorder with indices in unified array (0 to 1) + expect(mockOnUnifiedTabReorder).toHaveBeenCalledWith(0, 1); + // Should NOT call legacy onTabReorder since unified is available + expect(mockOnTabReorder).not.toHaveBeenCalled(); + }); + + it('drags file tab to AI tab position and calls onUnifiedTabReorder', () => { + render( + + ); + + const fileTabElement = screen.getByText('file1').closest('[data-tab-id]')!; + const aiTabElement = screen.getByText('AI Tab 2').closest('[data-tab-id]')!; + + // Start dragging file-tab-1 (index 1) + fireEvent.dragStart(fileTabElement, { + dataTransfer: { + effectAllowed: '', + setData: vi.fn(), + getData: vi.fn().mockReturnValue('file-tab-1'), + }, + }); + + // Drop on ai-tab-2 (index 2) + fireEvent.drop(aiTabElement, { + dataTransfer: { + getData: vi.fn().mockReturnValue('file-tab-1'), + }, + }); + + // Should call onUnifiedTabReorder (from index 1 to index 2) + expect(mockOnUnifiedTabReorder).toHaveBeenCalledWith(1, 2); + }); + + it('drags file tab to another file tab position', () => { + render( + + ); + + const fileTab1Element = screen.getByText('file1').closest('[data-tab-id]')!; + const fileTab2Element = screen.getByText('file2').closest('[data-tab-id]')!; + + // Start dragging file-tab-1 (index 1) + fireEvent.dragStart(fileTab1Element, { + dataTransfer: { + effectAllowed: '', + setData: vi.fn(), + getData: vi.fn().mockReturnValue('file-tab-1'), + }, + }); + + // Drop on file-tab-2 (index 3) + fireEvent.drop(fileTab2Element, { + dataTransfer: { + getData: vi.fn().mockReturnValue('file-tab-1'), + }, + }); + + // Should call onUnifiedTabReorder (from index 1 to index 3) + expect(mockOnUnifiedTabReorder).toHaveBeenCalledWith(1, 3); + }); + + it('does not reorder when dropping on the same tab', () => { + render( + + ); + + const fileTabElement = screen.getByText('file1').closest('[data-tab-id]')!; + + // Drop on same tab + fireEvent.drop(fileTabElement, { + dataTransfer: { + getData: vi.fn().mockReturnValue('file-tab-1'), + }, + }); + + expect(mockOnUnifiedTabReorder).not.toHaveBeenCalled(); + }); + + it('sets drag over visual feedback on target tab', () => { + render( + + ); + + const aiTabElement = screen.getByText('AI Tab 1').closest('[data-tab-id]')!; + const fileTabElement = screen.getByText('file1').closest('[data-tab-id]')!; + + // Start dragging AI tab + fireEvent.dragStart(aiTabElement, { + dataTransfer: { + effectAllowed: '', + setData: vi.fn(), + getData: vi.fn().mockReturnValue('ai-tab-1'), + }, + }); + + // Drag over file tab + fireEvent.dragOver(fileTabElement, { + dataTransfer: { + dropEffect: '', + }, + }); + + // File tab should have ring visual + expect(fileTabElement).toHaveClass('ring-2'); + }); + + it('uses legacy onTabReorder when unifiedTabs is not provided', () => { + render( + + ); + + const tab1 = screen.getByText('AI Tab 1').closest('[data-tab-id]')!; + const tab2 = screen.getByText('AI Tab 2').closest('[data-tab-id]')!; + + // Start dragging tab-1 + fireEvent.dragStart(tab1, { + dataTransfer: { + effectAllowed: '', + setData: vi.fn(), + getData: vi.fn().mockReturnValue('ai-tab-1'), + }, + }); + + // Drop on tab-2 + fireEvent.drop(tab2, { + dataTransfer: { + getData: vi.fn().mockReturnValue('ai-tab-1'), + }, + }); + + // Should use legacy onTabReorder + expect(mockOnTabReorder).toHaveBeenCalledWith(0, 1); + // Should NOT call onUnifiedTabReorder + expect(mockOnUnifiedTabReorder).not.toHaveBeenCalled(); + }); + + it('shows Move to First/Last for file tabs when not at edges', async () => { + render( + + ); + + // Hover over file1 (index 1, not first or last) + const fileTabElement = screen.getByText('file1').closest('[data-tab-id]')!; + + await act(async () => { + fireEvent.mouseEnter(fileTabElement); + vi.advanceTimersByTime(450); + }); + + // Should show both move options + expect(screen.getByText('Move to First Position')).toBeInTheDocument(); + expect(screen.getByText('Move to Last Position')).toBeInTheDocument(); + }); + + it('hides Move to First for first tab', async () => { + render( + + ); + + // Hover over AI Tab 1 (index 0, first tab) + const aiTabElement = screen.getByText('AI Tab 1').closest('[data-tab-id]')!; + + await act(async () => { + fireEvent.mouseEnter(aiTabElement); + vi.advanceTimersByTime(450); + }); + + // Move to First should be hidden (not just disabled) + expect(screen.queryByText('Move to First Position')).not.toBeInTheDocument(); + // Move to Last should be visible + expect(screen.getByText('Move to Last Position')).toBeInTheDocument(); + }); + + it('hides Move to Last for last tab', async () => { + render( + + ); + + // Hover over file2 (index 3, last tab) + const fileTabElement = screen.getByText('file2').closest('[data-tab-id]')!; + + await act(async () => { + fireEvent.mouseEnter(fileTabElement); + vi.advanceTimersByTime(450); + }); + + // Move to First should be visible + expect(screen.getByText('Move to First Position')).toBeInTheDocument(); + // Move to Last should be hidden (not just disabled) + expect(screen.queryByText('Move to Last Position')).not.toBeInTheDocument(); + }); + + it('calls onUnifiedTabReorder when Move to First is clicked on file tab', async () => { + render( + + ); + + // Hover over file1 (index 1) + const fileTabElement = screen.getByText('file1').closest('[data-tab-id]')!; + + await act(async () => { + fireEvent.mouseEnter(fileTabElement); + vi.advanceTimersByTime(450); + }); + + // Click Move to First + const moveButton = screen.getByText('Move to First Position'); + fireEvent.click(moveButton); + + // Should call onUnifiedTabReorder with index 1 -> 0 + expect(mockOnUnifiedTabReorder).toHaveBeenCalledWith(1, 0); + }); + + it('calls onUnifiedTabReorder when Move to Last is clicked on file tab', async () => { + render( + + ); + + // Hover over file1 (index 1) + const fileTabElement = screen.getByText('file1').closest('[data-tab-id]')!; + + await act(async () => { + fireEvent.mouseEnter(fileTabElement); + vi.advanceTimersByTime(450); + }); + + // Click Move to Last + const moveButton = screen.getByText('Move to Last Position'); + fireEvent.click(moveButton); + + // Should call onUnifiedTabReorder with index 1 -> 3 (last index) + expect(mockOnUnifiedTabReorder).toHaveBeenCalledWith(1, 3); + }); + + it('middle-click closes file tab', () => { + render( + + ); + + const fileTabElement = screen.getByText('file1').closest('[data-tab-id]')!; + + // Middle-click on file tab + fireEvent.mouseDown(fileTabElement, { button: 1 }); + + expect(mockOnFileTabClose).toHaveBeenCalledWith('file-tab-1'); + }); +});