diff --git a/frontend/src/components/repo/RepoList.test.tsx b/frontend/src/components/repo/RepoList.test.tsx new file mode 100644 index 000000000..f61bf1c78 --- /dev/null +++ b/frontend/src/components/repo/RepoList.test.tsx @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { createUseSettingsMock } from '@/test/test-utils' +import { RepoList } from './RepoList' + +const mocks = vi.hoisted(() => ({ listRepos: vi.fn() })) + +vi.mock('@/api/repos', () => ({ + listRepos: mocks.listRepos, + deleteRepo: vi.fn(), + updateRepoOrder: vi.fn(), +})) + +vi.mock('@/hooks/useSettings', () => ({ + useSettings: () => createUseSettingsMock(), +})) + +vi.mock('@/hooks/useMobile', () => ({ useMobile: () => true })) + +function renderRepoList(onAddRepo: () => void) { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + + return render( + + + , + ) +} + +describe('RepoList', () => { + it('calls onAddRepo once from the empty state', async () => { + mocks.listRepos.mockResolvedValue([]) + const onAddRepo = vi.fn() + const user = userEvent.setup() + + renderRepoList(onAddRepo) + + await user.click(await screen.findByRole('button', { name: 'Add repository' })) + + expect(onAddRepo).toHaveBeenCalledTimes(1) + }) +}) diff --git a/frontend/src/components/repo/RepoList.tsx b/frontend/src/components/repo/RepoList.tsx index b222b2af2..968f3af41 100644 --- a/frontend/src/components/repo/RepoList.tsx +++ b/frontend/src/components/repo/RepoList.tsx @@ -6,8 +6,9 @@ import { CSS } from "@dnd-kit/utilities" import { listRepos, deleteRepo, updateRepoOrder, type DeleteBranchChoice } from "@/api/repos" import { fetchReposGitStatus } from "@/api/git" import { DeleteDialog } from "@/components/ui/delete-dialog" +import { Button } from "@/components/ui/button" import { RadioOptionGroup, type RadioOption } from "@/components/ui/radio-option-group" -import { GitBranch, Search, GripVertical } from "lucide-react" +import { GitBranch, Search, GripVertical, Plus } from "lucide-react" import type { Repo } from "@/api/types" import type { GitStatusResponse } from "@/types/git" import { RepoCard } from "./RepoCard" @@ -135,7 +136,7 @@ function StaticRepoCard({ ) } -export function RepoList() { +export function RepoList({ onAddRepo }: { onAddRepo: () => void }) { const queryClient = useQueryClient() const isMobile = useMobile() const { preferences, updateSettings } = useSettings() @@ -341,6 +342,10 @@ export function RepoList() {

No repositories yet. Add one to get started.

+ ) @@ -413,6 +418,7 @@ export function RepoList() { hasClonedRepos={hasClonedRepos} selectionMode={selectionMode} onSelectionModeChange={handleSelectionModeChange} + onAddRepo={onAddRepo} />
diff --git a/frontend/src/components/repo/RepoListControls.test.tsx b/frontend/src/components/repo/RepoListControls.test.tsx new file mode 100644 index 000000000..f4b2ceb36 --- /dev/null +++ b/frontend/src/components/repo/RepoListControls.test.tsx @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { RepoListControls } from './RepoListControls' + +const mocks = vi.hoisted(() => ({ mobile: { current: false } })) + +vi.mock('@/hooks/useMobile', () => ({ + isMobileViewport: () => mocks.mobile.current, + useMobile: () => mocks.mobile.current, +})) + +function baseProps() { + return { + searchQuery: '', + onSearchChange: vi.fn(), + filterMode: 'recent' as const, + onFilterModeChange: vi.fn(), + sortMode: 'recent' as const, + onSortModeChange: vi.fn(), + filteredCount: 3, + attentionCount: 0, + selectedCount: 0, + allVisibleSelected: false, + onSelectAll: vi.fn(), + onClearSelection: vi.fn(), + onDelete: vi.fn(), + hasLocalRepos: false, + hasClonedRepos: false, + selectionMode: false, + onSelectionModeChange: vi.fn(), + onAddRepo: vi.fn(), + } +} + +describe('RepoListControls', () => { + beforeEach(() => { + mocks.mobile.current = false + }) + + it('renders the add repository button after the search input on mobile', () => { + mocks.mobile.current = true + + render() + + const search = screen.getByRole('textbox') + const addButton = screen.getByRole('button', { name: 'Add repository' }) + + expect( + search.compareDocumentPosition(addButton) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy() + }) + + it('calls onAddRepo once when the mobile button is clicked', async () => { + mocks.mobile.current = true + const props = baseProps() + const user = userEvent.setup() + + render() + await user.click(screen.getByRole('button', { name: 'Add repository' })) + + expect(props.onAddRepo).toHaveBeenCalledTimes(1) + }) + + it('does not render the add repository button on desktop', () => { + render() + + expect(screen.queryByRole('button', { name: 'Add repository' })).not.toBeInTheDocument() + }) +}) diff --git a/frontend/src/components/repo/RepoListControls.tsx b/frontend/src/components/repo/RepoListControls.tsx index ba24ca882..ce069de7e 100644 --- a/frontend/src/components/repo/RepoListControls.tsx +++ b/frontend/src/components/repo/RepoListControls.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { SlidersHorizontal, Trash2, X } from "lucide-react"; +import { Plus, SlidersHorizontal, Trash2, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { RepoSearchInput } from "./RepoSearchInput"; import { @@ -31,6 +31,7 @@ interface RepoListControlsProps { hasClonedRepos: boolean; selectionMode: boolean; onSelectionModeChange: (enabled: boolean) => void; + onAddRepo: () => void; } const FILTER_OPTIONS: { value: RepoFilterMode; label: string }[] = [ @@ -65,6 +66,7 @@ export function RepoListControls({ hasClonedRepos, selectionMode, onSelectionModeChange, + onAddRepo, }: RepoListControlsProps) { const isMobile = useMobile(); const [showMenu, setShowMenu] = useState(false); @@ -122,10 +124,29 @@ export function RepoListControls({
+ {isMobile && ( + + )} + {isMobile ? ( - diff --git a/frontend/src/pages/Repos.tsx b/frontend/src/pages/Repos.tsx index 753858c0f..394b22f8a 100644 --- a/frontend/src/pages/Repos.tsx +++ b/frontend/src/pages/Repos.tsx @@ -62,7 +62,11 @@ export function Repos() { All Schedules {isLocalServer ? ( - @@ -73,7 +77,7 @@ export function Repos() {
- {isLocalServer ? : } + {isLocalServer ? setAddRepoOpen(true)} /> : }
{isLocalServer ? (