/** * @vitest-environment happy-dom */ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vitest'; import { act, render } from '@testing-library/react'; import React from 'react'; import { Window } from 'happy-dom'; import { App } from '../../components/App.js'; import type { BranchInfo, BranchItem } from '../../types.js'; import * as useGitDataModule from '../../hooks/useGitData.js'; import * as useScreenStateModule from '../../hooks/useScreenState.js'; import * as BranchListScreenModule from '../../components/screens/BranchListScreen.js'; import * as BranchActionSelectorScreenModule from '../../screens/BranchActionSelectorScreen.js'; import * as worktreeModule from '../../../worktree.js'; import * as gitModule from '../../../git.js'; import type { ScreenType } from '../../types.js'; const navigateToMock = vi.fn(); const goBackMock = vi.fn(); const resetMock = vi.fn(); const originalUseGitData = useGitDataModule.useGitData; const originalUseScreenState = useScreenStateModule.useScreenState; const originalBranchListScreen = BranchListScreenModule.BranchListScreen; const originalBranchActionSelector = BranchActionSelectorScreenModule.BranchActionSelectorScreen; const originalGetRepositoryRoot = gitModule.getRepositoryRoot; const useGitDataSpy = vi.spyOn(useGitDataModule, 'useGitData'); const useScreenStateSpy = vi.spyOn(useScreenStateModule, 'useScreenState'); const branchListScreenSpy = vi.spyOn(BranchListScreenModule, 'BranchListScreen'); const branchActionSelectorSpy = vi.spyOn(BranchActionSelectorScreenModule, 'BranchActionSelectorScreen'); const switchToProtectedBranchSpy = vi.spyOn(worktreeModule, 'switchToProtectedBranch'); const getRepositoryRootSpy = vi.spyOn(gitModule, 'getRepositoryRoot'); const branchListProps: any[] = []; const branchActionProps: any[] = []; const aiToolProps: any[] = []; let currentScreenState: ScreenType; vi.mock('../../components/screens/AIToolSelectorScreen.js', () => { return { AIToolSelectorScreen: (props: unknown) => { aiToolProps.push(props); return React.createElement('div'); }, }; }); describe('App protected branch handling', () => { beforeEach(() => { const window = new Window(); globalThis.window = window as any; globalThis.document = window.document as any; currentScreenState = 'branch-list'; navigateToMock.mockReset(); goBackMock.mockReset(); resetMock.mockReset(); branchListProps.length = 0; branchActionProps.length = 0; aiToolProps.length = 0; useGitDataSpy.mockReset(); switchToProtectedBranchSpy.mockReset(); getRepositoryRootSpy.mockReset(); useScreenStateSpy.mockImplementation(() => ({ currentScreen: currentScreenState, navigateTo: (screen: ScreenType) => { navigateToMock(screen); currentScreenState = screen; }, goBack: goBackMock, reset: () => { resetMock(); currentScreenState = 'branch-list'; }, })); branchListScreenSpy.mockImplementation((props: any) => { branchListProps.push(props); return React.createElement(originalBranchListScreen, props); }); branchActionSelectorSpy.mockImplementation((props: any) => { branchActionProps.push(props); return React.createElement(originalBranchActionSelector, props); }); switchToProtectedBranchSpy.mockResolvedValue('local'); getRepositoryRootSpy.mockResolvedValue('/repo'); }); afterEach(() => { useGitDataSpy.mockReset(); useGitDataSpy.mockImplementation(originalUseGitData); useScreenStateSpy.mockReset(); useScreenStateSpy.mockImplementation(originalUseScreenState); branchListScreenSpy.mockImplementation(originalBranchListScreen as any); branchActionSelectorSpy.mockImplementation(originalBranchActionSelector as any); switchToProtectedBranchSpy.mockReset(); getRepositoryRootSpy.mockReset(); branchActionProps.length = 0; }); afterAll(() => { useGitDataSpy.mockRestore(); useScreenStateSpy.mockRestore(); branchListScreenSpy.mockRestore(); branchActionSelectorSpy.mockRestore(); switchToProtectedBranchSpy.mockRestore(); getRepositoryRootSpy.mockRestore(); }); it('shows protected branch warning and switches root without launching AI tool', async () => { const branches: BranchInfo[] = [ { name: 'main', type: 'local', branchType: 'main', isCurrent: false, }, { name: 'feature/example', type: 'local', branchType: 'feature', isCurrent: true, }, ]; useGitDataSpy.mockImplementation(() => ({ branches, worktrees: [], loading: false, error: null, refresh: vi.fn(), lastUpdated: null, })); render(); expect(branchListProps).not.toHaveLength(0); const latestProps = branchListProps.at(-1); expect(latestProps).toBeDefined(); if (!latestProps) { throw new Error('BranchListScreen props missing'); } const protectedBranch = (latestProps.branches as BranchItem[]).find( (item) => item.name === 'main' ); expect(protectedBranch).toBeDefined(); if (!protectedBranch) { throw new Error('Protected branch item not found'); } await act(async () => { latestProps.onSelect(protectedBranch); await Promise.resolve(); }); expect(navigateToMock).toHaveBeenCalledWith('branch-action-selector'); expect(branchActionProps).not.toHaveLength(0); const actionProps = branchActionProps.at(-1); expect(actionProps?.mode).toBe('protected'); expect(actionProps?.infoMessage).toContain('ルートブランチ'); expect(actionProps?.primaryLabel).toBe('Use root branch (no worktree)'); expect(actionProps?.secondaryLabel).toBe('Create new branch from this branch'); await act(async () => { actionProps?.onUseExisting(); await Promise.resolve(); await Promise.resolve(); }); expect(switchToProtectedBranchSpy).toHaveBeenCalledWith({ branchName: 'main', repoRoot: expect.any(String), remoteRef: null, }); expect(navigateToMock).toHaveBeenCalledWith('ai-tool-selector'); expect(aiToolProps).not.toHaveLength(0); }); });