/** * @vitest-environment happy-dom */ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest'; import type { Mock } from 'vitest'; import { render, waitFor } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import React from 'react'; import { App } from '../../components/App.js'; import { Window } from 'happy-dom'; import type { BranchInfo, BranchItem } from '../../types.js'; import * as BranchListScreenModule from '../../components/screens/BranchListScreen.js'; import * as BranchActionSelectorScreenModule from '../../screens/BranchActionSelectorScreen.js'; vi.mock('../../../git.js', () => ({ __esModule: true, getAllBranches: vi.fn(), getRepositoryRoot: vi.fn(async () => '/repo'), deleteBranch: vi.fn(async () => undefined), })); const { mockIsProtectedBranchName, mockSwitchToProtectedBranch } = vi.hoisted(() => ({ mockIsProtectedBranchName: vi.fn(() => false), mockSwitchToProtectedBranch: vi.fn(async () => 'none' as const), })); vi.mock('../../../worktree.js', () => ({ __esModule: true, listAdditionalWorktrees: vi.fn(), createWorktree: vi.fn(async () => undefined), generateWorktreePath: vi.fn(async () => '/repo/.git/worktree/test'), getMergedPRWorktrees: vi.fn(async () => []), removeWorktree: vi.fn(async () => undefined), isProtectedBranchName: mockIsProtectedBranchName, switchToProtectedBranch: mockSwitchToProtectedBranch, })); const aiToolScreenProps: unknown[] = []; vi.mock('../../components/screens/AIToolSelectorScreen.js', () => { return { AIToolSelectorScreen: (props: unknown) => { aiToolScreenProps.push(props); return React.createElement('div'); }, }; }); import { getAllBranches, getRepositoryRoot, deleteBranch } from '../../../git.js'; import { listAdditionalWorktrees, createWorktree, generateWorktreePath, getMergedPRWorktrees, removeWorktree, } from '../../../worktree.js'; const mockedGetAllBranches = getAllBranches as Mock; const mockedGetRepositoryRoot = getRepositoryRoot as Mock; const mockedDeleteBranch = deleteBranch as Mock; const mockedListAdditionalWorktrees = listAdditionalWorktrees as Mock; const mockedCreateWorktree = createWorktree as Mock; const mockedGenerateWorktreePath = generateWorktreePath as Mock; const mockedGetMergedPRWorktrees = getMergedPRWorktrees as Mock; const mockedRemoveWorktree = removeWorktree as Mock; const mockedIsProtectedBranchName = mockIsProtectedBranchName as Mock; const mockedSwitchToProtectedBranch = mockSwitchToProtectedBranch as Mock; const originalBranchListScreen = BranchListScreenModule.BranchListScreen; const originalBranchActionSelectorScreen = BranchActionSelectorScreenModule.BranchActionSelectorScreen; describe('Navigation Integration Tests', () => { beforeEach(() => { // Setup happy-dom const window = new Window(); globalThis.window = window as any; globalThis.document = window.document as any; // Reset mocks mockedGetAllBranches.mockReset(); mockedListAdditionalWorktrees.mockReset(); mockedGetRepositoryRoot.mockReset(); mockedDeleteBranch.mockReset(); mockedCreateWorktree.mockReset(); mockedGenerateWorktreePath.mockReset(); mockedGetMergedPRWorktrees.mockReset(); mockedRemoveWorktree.mockReset(); mockedIsProtectedBranchName.mockReset(); mockedSwitchToProtectedBranch.mockReset(); mockedGetRepositoryRoot.mockResolvedValue('/repo'); mockedSwitchToProtectedBranch.mockResolvedValue('local'); }); const mockBranches: BranchInfo[] = [ { name: 'main', type: 'local', branchType: 'main', isCurrent: true, }, { name: 'feature/test', type: 'local', branchType: 'feature', isCurrent: false, }, ]; it('should start with branch-list screen', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { getByText } = render(); await waitFor(() => { expect(getByText(/Claude Worktree/i)).toBeDefined(); expect(getByText(/main/)).toBeDefined(); }); }); it('should support navigation between screens', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { container } = render(); await waitFor(() => { expect(container).toBeDefined(); }); // Test will verify screen navigation expect(container).toBeDefined(); }); it('should maintain state across screen transitions', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { container } = render(); await waitFor(() => { expect(container).toBeDefined(); }); // Test will verify state persistence expect(container).toBeDefined(); }); it('should handle back navigation correctly', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { container } = render(); await waitFor(() => { expect(container).toBeDefined(); }); // Test will verify back navigation expect(container).toBeDefined(); }); it('should handle navigation history', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { container } = render(); await waitFor(() => { expect(container).toBeDefined(); }); // Test will verify navigation history expect(container).toBeDefined(); }); it('should display correct screen on navigation', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { container } = render(); await waitFor(() => { expect(container).toBeDefined(); }); // Test will verify correct screen rendering expect(container).toBeDefined(); }); it('should call onExit when branch is selected', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { container } = render(); await waitFor(() => { expect(container).toBeDefined(); }); // Test will verify onExit is called expect(container).toBeDefined(); }); }); describe('Protected Branch Navigation (T103)', () => { const branchListProps: any[] = []; const branchActionProps: any[] = []; let branchListSpy: ReturnType; let branchActionSpy: ReturnType; const baseBranches: BranchInfo[] = [ { name: 'main', type: 'local', branchType: 'main', isCurrent: true, }, { name: 'feature/test', type: 'local', branchType: 'feature', isCurrent: false, }, ]; beforeEach(() => { const window = new Window(); globalThis.window = window as any; globalThis.document = window.document as any; mockedGetAllBranches.mockReset(); mockedListAdditionalWorktrees.mockReset(); mockedGetRepositoryRoot.mockReset(); mockedDeleteBranch.mockReset(); mockedCreateWorktree.mockReset(); mockedGenerateWorktreePath.mockReset(); mockedGetMergedPRWorktrees.mockReset(); mockedRemoveWorktree.mockReset(); mockedIsProtectedBranchName.mockReset(); mockedSwitchToProtectedBranch.mockReset(); mockedGetRepositoryRoot.mockResolvedValue('/repo'); branchListProps.length = 0; branchActionProps.length = 0; aiToolScreenProps.length = 0; branchListSpy = vi .spyOn(BranchListScreenModule, 'BranchListScreen') .mockImplementation((props: any) => { branchListProps.push(props); return React.createElement(originalBranchListScreen, props); }); branchActionSpy = vi .spyOn(BranchActionSelectorScreenModule, 'BranchActionSelectorScreen') .mockImplementation((props: any) => { branchActionProps.push(props); return React.createElement(originalBranchActionSelectorScreen, props); }); mockedIsProtectedBranchName.mockImplementation((name: string) => ['main', 'develop', 'origin/main', 'origin/develop'].includes(name) ); mockedSwitchToProtectedBranch.mockResolvedValue('local'); mockedGetRepositoryRoot.mockResolvedValue('/repo'); }); afterEach(() => { branchListSpy.mockRestore(); branchActionSpy.mockRestore(); }); it('switches local protected branches via root workflow and navigates to AI tool', async () => { mockedGetAllBranches.mockResolvedValue(baseBranches); mockedListAdditionalWorktrees.mockResolvedValue([]); const onExit = vi.fn(); render(); await waitFor(() => { expect(branchListProps.length).toBeGreaterThan(0); }); await waitFor(() => { const latest = branchListProps.at(-1); const names = (latest?.branches as BranchItem[] | undefined)?.map( (item) => item.name ); expect(names).toBeDefined(); expect(names).toContain('main'); }); const latestProps = branchListProps.at(-1); const protectedBranch = (latestProps?.branches as BranchItem[]).find( (item) => item.name === 'main' ); expect(protectedBranch).toBeDefined(); await act(async () => { latestProps?.onSelect(protectedBranch); await Promise.resolve(); }); await waitFor(() => { expect(branchActionProps.length).toBeGreaterThan(0); }); const actionProps = branchActionProps.at(-1); expect(actionProps?.mode).toBe('protected'); expect(actionProps?.infoMessage).toContain('ルートブランチ'); await act(async () => { await actionProps?.onUseExisting(); await Promise.resolve(); }); expect(mockedSwitchToProtectedBranch).toHaveBeenCalledWith({ branchName: 'main', repoRoot: '/repo', remoteRef: null, }); await waitFor(() => { expect(aiToolScreenProps.length).toBeGreaterThan(0); }); }); it('creates tracking branch for remote protected selections before navigating to AI tool', async () => { const remoteBranches: BranchInfo[] = [ { name: 'origin/develop', type: 'remote', branchType: 'develop', isCurrent: false, }, { name: 'feature/test', type: 'local', branchType: 'feature', isCurrent: false, }, ]; mockedGetAllBranches.mockResolvedValue(remoteBranches); mockedListAdditionalWorktrees.mockResolvedValue([]); mockedSwitchToProtectedBranch.mockResolvedValue('remote'); const onExit = vi.fn(); render(); await waitFor(() => { expect(branchListProps.length).toBeGreaterThan(0); }); await waitFor(() => { const latest = branchListProps.at(-1); const names = (latest?.branches as BranchItem[] | undefined)?.map( (item) => item.name ); expect(names).toBeDefined(); expect(names).toContain('origin/develop'); }); const latestProps = branchListProps.at(-1); const protectedBranch = (latestProps?.branches as BranchItem[]).find( (item) => item.name === 'origin/develop' ); expect(protectedBranch).toBeDefined(); await act(async () => { latestProps?.onSelect(protectedBranch); await Promise.resolve(); }); await waitFor(() => { expect(branchActionProps.length).toBeGreaterThan(0); }); const actionProps = branchActionProps.at(-1); expect(actionProps?.mode).toBe('protected'); expect(actionProps?.primaryLabel).toContain('root'); await act(async () => { await actionProps?.onUseExisting(); await Promise.resolve(); }); expect(mockedSwitchToProtectedBranch).toHaveBeenCalledWith({ branchName: 'develop', repoRoot: '/repo', remoteRef: 'origin/develop', }); await waitFor(() => { expect(aiToolScreenProps.length).toBeGreaterThan(0); }); }); }); afterAll(() => { vi.restoreAllMocks(); });