import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { worktreeApi } from '../../services/api/worktreeApi'; import { revealPath } from '../../utils/openUrl'; import WorktreeActions from './WorktreeActions'; vi.mock('../../services/api/worktreeApi', () => ({ worktreeApi: { diff: vi.fn(), remove: vi.fn() }, })); vi.mock('../../utils/openUrl', () => ({ revealPath: vi.fn().mockResolvedValue(undefined) })); vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) })); const mockDiff = vi.mocked(worktreeApi.diff); const mockRemove = vi.mocked(worktreeApi.remove); const mockReveal = vi.mocked(revealPath); const PATH = '/r/.claude/worktrees/worker-a'; describe('WorktreeActions', () => { beforeEach(() => { mockDiff.mockReset(); mockRemove.mockReset().mockResolvedValue(true); mockReveal.mockReset().mockResolvedValue(undefined); }); it('open reveals the path in the OS file manager', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByTestId('worktree-open')); expect(mockReveal).toHaveBeenCalledWith(PATH); }); it('diff fetches once and renders the summary, toggling closed on second click', async () => { mockDiff.mockResolvedValue(' src/a.rs | 2 +-'); const user = userEvent.setup(); render(); await user.click(screen.getByTestId('worktree-diff')); await waitFor(() => expect(screen.getByTestId('worktree-diff-output')).toHaveTextContent('src/a.rs') ); expect(mockDiff).toHaveBeenCalledTimes(1); await user.click(screen.getByTestId('worktree-diff')); expect(screen.queryByTestId('worktree-diff-output')).toBeNull(); }); it('removes a clean worktree in one click (force=false)', async () => { const onRemoved = vi.fn(); const user = userEvent.setup(); render(); await user.click(screen.getByTestId('worktree-remove')); await waitFor(() => expect(mockRemove).toHaveBeenCalledWith(PATH, false)); expect(onRemoved).toHaveBeenCalledWith(PATH); }); it('a dirty worktree requires explicit confirmation before removal', async () => { const onRemoved = vi.fn(); const user = userEvent.setup(); render(); await user.click(screen.getByTestId('worktree-remove')); // No removal yet — a confirm row appears instead. expect(mockRemove).not.toHaveBeenCalled(); expect(screen.getByTestId('worktree-remove-confirm')).toBeInTheDocument(); // Discard & remove forces. await user.click(screen.getByTestId('worktree-remove-confirm-yes')); await waitFor(() => expect(mockRemove).toHaveBeenCalledWith(PATH, true)); expect(onRemoved).toHaveBeenCalledWith(PATH); }); it('resets the removing state after success when no onRemoved parent drops the row', async () => { // Inline timeline use (ToolTimelineBlock) renders without onRemoved, so the // component is never unmounted by a parent — it must reset its own state or // the button stays stuck on "worktree.removing" with disabled forever. const user = userEvent.setup(); render(); await user.click(screen.getByTestId('worktree-remove')); await waitFor(() => expect(mockRemove).toHaveBeenCalledWith(PATH, false)); await waitFor(() => { const btn = screen.getByTestId('worktree-remove'); expect(btn).toHaveTextContent('worktree.action.remove'); expect(btn).not.toBeDisabled(); }); }); it('preserve cancels the dirty-removal confirm without removing', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByTestId('worktree-remove')); await user.click(screen.getByTestId('worktree-preserve')); expect(mockRemove).not.toHaveBeenCalled(); expect(screen.queryByTestId('worktree-remove-confirm')).toBeNull(); expect(screen.getByTestId('worktree-remove')).toBeInTheDocument(); }); });