/** * @vitest-environment happy-dom * Acceptance tests for User Story 2: Sub-screen Navigation */ import { describe, it, expect, beforeEach, afterAll, vi } from 'vitest'; import type { Mock } from 'vitest'; import { render, waitFor } 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'; // Mock git.js and worktree.js vi.mock('../../../git.js', () => ({ __esModule: true, getAllBranches: vi.fn(), getRepositoryRoot: vi.fn(async () => '/repo'), deleteBranch: vi.fn(async () => undefined), })); const { acceptanceIsProtectedBranchName, acceptanceSwitchToProtectedBranch } = vi.hoisted(() => ({ acceptanceIsProtectedBranchName: vi.fn(() => false), acceptanceSwitchToProtectedBranch: 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: acceptanceIsProtectedBranchName, switchToProtectedBranch: acceptanceSwitchToProtectedBranch, })); 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 = acceptanceIsProtectedBranchName as Mock; const mockedSwitchToProtectedBranch = acceptanceSwitchToProtectedBranch as Mock; describe('Acceptance: Navigation (User Story 2)', () => { 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('none'); }); const mockBranches: BranchInfo[] = [ { name: 'main', type: 'local', branchType: 'main', isCurrent: true, }, { name: 'feature/test', type: 'local', branchType: 'feature', isCurrent: false, }, ]; /** * T074: Acceptance Scenario 1 * nキーで新規ブランチ作成画面に遷移 */ it('[AC1] should navigate to branch creator on n key', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { getByText, container } = render(); await waitFor(() => { expect(getByText(/Claude Worktree/i)).toBeDefined(); }); // Verify n key action is available in footer const nKeyElements = container.querySelectorAll('*'); let hasNKey = false; nKeyElements.forEach((el) => { if (el.textContent?.toLowerCase().includes('new branch')) { hasNKey = true; } }); expect(hasNKey || container.textContent?.toLowerCase().includes('n')).toBe(true); }); /** * T075: Acceptance Scenario 2 * メイン画面にはqキーが存在しない(終了はCtrl+Cのみ) */ it('[AC2] should not have q key on main screen', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { container } = render(); await waitFor(() => { expect(container).toBeDefined(); }); // Verify q key is NOT in the footer (main screen uses Ctrl+C for exit) const footerText = container.textContent || ''; // Main screen should not have 'q' for quit, but should have other keys expect(footerText.toLowerCase()).not.toMatch(/\[q\]/); expect(footerText.toLowerCase()).toContain('enter'); }); /** * T076: Acceptance Scenario 3 * Worktree管理でアクション実行後に適切に遷移 */ it('[AC3] should handle worktree management navigation', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([ { branch: 'feature/test', path: '/path/to/worktree', head: 'abc123', isAccessible: true, }, ]); const onExit = vi.fn(); const { getByText, container } = render(); await waitFor(() => { expect(getByText(/Claude Worktree/i)).toBeDefined(); }); // Verify m key action is available for worktree management const mKeyElements = container.querySelectorAll('*'); let hasMKey = false; mKeyElements.forEach((el) => { if (el.textContent?.toLowerCase().includes('manage worktrees')) { hasMKey = true; } }); expect(hasMKey || container.textContent?.toLowerCase().includes('m')).toBe(true); }); it('[Integration] should support all navigation keys', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { getByText, getAllByText } = render(); await waitFor(() => { expect(getByText(/Claude Worktree/i)).toBeDefined(); }); // Verify navigation keys are available (main screen doesn't have q key) const enterKeys = getAllByText(/enter/i); expect(enterKeys.length).toBeGreaterThan(0); }); it('[Integration] should display correct footer actions', async () => { (getAllBranches as ReturnType).mockResolvedValue(mockBranches); (listAdditionalWorktrees as ReturnType).mockResolvedValue([]); const onExit = vi.fn(); const { container } = render(); await waitFor(() => { expect(container).toBeDefined(); }); // Verify footer has multiple action keys (main screen doesn't have q key) const footerText = container.textContent || ''; expect(footerText.toLowerCase()).toContain('enter'); expect(footerText.toLowerCase()).toContain('m'); // Manage worktrees }); }); afterAll(() => { vi.restoreAllMocks(); });