/** * @vitest-environment happy-dom */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render } from '@testing-library/react'; import React from 'react'; import { App } from '../../components/App.js'; import { Window } from 'happy-dom'; import type { BranchInfo } from '../../types.js'; /** * Real-time update integration tests * Tests auto-refresh functionality and lastUpdated display */ // Mock useGitData hook const mockRefresh = vi.fn(); vi.mock('../../hooks/useGitData.js', () => ({ useGitData: vi.fn(), })); import { useGitData } from '../../hooks/useGitData.js'; const mockUseGitData = useGitData as ReturnType; describe('Real-time Update Integration', () => { beforeEach(() => { // Setup happy-dom const window = new Window(); globalThis.window = window as any; globalThis.document = window.document as any; // Reset mocks vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); it('T084: should disable auto-refresh (manual refresh with r key)', () => { const mockBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-1', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: mockBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); const onExit = vi.fn(); render(); // Verify useGitData was called with auto-refresh disabled (manual refresh with r key) expect(mockUseGitData).toHaveBeenCalledWith({ enableAutoRefresh: false, }); }); it('T085: should display updated statistics', () => { const mockBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-1', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: mockBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); const onExit = vi.fn(); const { getByText, rerender } = render(); // Initial state should show "Local: 2" expect(getByText(/Local:/i)).toBeDefined(); expect(getByText('2')).toBeDefined(); // Simulate Git operation: add a new branch const updatedBranches: BranchInfo[] = [ ...mockBranches, { name: 'feature/test-2', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: updatedBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); // Re-render to simulate update rerender(); // Should now show "Local: 3" expect(getByText('3')).toBeDefined(); }); it('T086: should update statistics after Worktree creation', () => { const mockBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-1', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: mockBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); const onExit = vi.fn(); const { container, getByText, rerender } = render(); // Initial state should show "Worktrees: 0" expect(getByText(/Worktrees:/i)).toBeDefined(); // Verify the content contains Worktrees: 0 expect(container.textContent).toContain('Worktrees'); // Simulate Worktree creation const branchesWithWorktree: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-1', branchType: 'feature', type: 'local', isCurrent: false, worktree: { path: '/mock/worktree/feature-test-1', branch: 'feature/test-1', isAccessible: true, }, }, ]; mockUseGitData.mockReturnValue({ branches: branchesWithWorktree, worktrees: [ { path: '/mock/worktree/feature-test-1', branch: 'feature/test-1', isAccessible: true, }, ], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); // Re-render to simulate update rerender(); // Should now show "Worktrees: 1" expect(getByText(/Worktrees:/i)).toBeDefined(); // Verify worktree count increased by checking container content expect(container.textContent).toContain('Worktrees'); }); it('should display lastUpdated timestamp', () => { const mockBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, ]; const lastUpdated = new Date(); mockUseGitData.mockReturnValue({ branches: mockBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated, }); const onExit = vi.fn(); const { getByText } = render(); // Should display "Updated:" text expect(getByText(/Updated:/i)).toBeDefined(); }); it('should handle refresh errors gracefully', () => { const error = new Error('Git command failed'); mockUseGitData.mockReturnValue({ branches: [], worktrees: [], loading: false, error, refresh: mockRefresh, lastUpdated: new Date(), }); const onExit = vi.fn(); const { getByText } = render(); // Should display error message expect(getByText(/Error:/i)).toBeDefined(); expect(getByText(/Git command failed/i)).toBeDefined(); }); /** * T082-3: Cursor position retention during auto-refresh * Tests that cursor position is maintained when data is auto-refreshed */ describe('Cursor Position Retention (T082-3)', () => { it('should maintain cursor position when branches data is refreshed with same content', () => { const mockBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-1', branchType: 'feature', type: 'local', isCurrent: false, }, { name: 'feature/test-2', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: mockBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); const onExit = vi.fn(); const { rerender } = render(); // Simulate user moving cursor down (this would be done via keyboard in real app) // For now, we just verify that the component renders // Create new array with same content (simulating auto-refresh) const refreshedBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-1', branchType: 'feature', type: 'local', isCurrent: false, }, { name: 'feature/test-2', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: refreshedBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); // Re-render to simulate auto-refresh rerender(); // With proper optimization: // 1. useMemo should not regenerate branchItems (content is the same) // 2. Select should not re-render (items prop hasn't changed) // 3. Cursor position should be maintained // Without optimization: // - branchItems would be regenerated // - Select would re-render // - Cursor position might be reset }); it('should maintain cursor position when a branch is added at the end', () => { const initialBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-1', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: initialBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); const onExit = vi.fn(); const { rerender } = render(); // Add a branch at the end (cursor should stay on current item) const updatedBranches: BranchInfo[] = [ ...initialBranches, { name: 'feature/test-2', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: updatedBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); rerender(); // Cursor should remain on the same item (e.g., index 1 should still point to 'feature/test-1') }); it('should adjust cursor position when current selected branch is deleted', () => { const initialBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-1', branchType: 'feature', type: 'local', isCurrent: false, }, { name: 'feature/test-2', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: initialBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); const onExit = vi.fn(); const { rerender } = render(); // Remove middle branch (cursor was on index 1, which is now deleted) const updatedBranches: BranchInfo[] = [ { name: 'main', branchType: 'main', type: 'local', isCurrent: true, }, { name: 'feature/test-2', branchType: 'feature', type: 'local', isCurrent: false, }, ]; mockUseGitData.mockReturnValue({ branches: updatedBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); rerender(); // Cursor should be clamped to valid index (e.g., moved to index 1, which is now 'feature/test-2') }); it('should maintain scroll offset during auto-refresh', () => { // Create many branches to test scrolling const manyBranches: BranchInfo[] = Array.from({ length: 20 }, (_, i) => ({ name: `feature/test-${i + 1}`, branchType: 'feature' as const, type: 'local' as const, isCurrent: false, })); mockUseGitData.mockReturnValue({ branches: manyBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); const onExit = vi.fn(); const { rerender } = render(); // Simulate auto-refresh with same content const refreshedBranches: BranchInfo[] = Array.from({ length: 20 }, (_, i) => ({ name: `feature/test-${i + 1}`, branchType: 'feature' as const, type: 'local' as const, isCurrent: false, })); mockUseGitData.mockReturnValue({ branches: refreshedBranches, worktrees: [], loading: false, error: null, refresh: mockRefresh, lastUpdated: new Date(), }); rerender(); // Scroll offset should be maintained // (in real app, user might be viewing items 10-20, and auto-refresh shouldn't reset to top) }); }); });