diff --git a/src/components/ui/LoadMoreButton.test.tsx b/src/components/ui/LoadMoreButton.test.tsx new file mode 100644 index 0000000..14c1024 --- /dev/null +++ b/src/components/ui/LoadMoreButton.test.tsx @@ -0,0 +1,150 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { LoadMoreButton } from './LoadMoreButton' + +describe('LoadMoreButton', () => { + it('renders button when hasNextPage is true', () => { + const mockOnLoadMore = vi.fn() + render( + + ) + + const button = screen.getByRole('button', { name: 'Load more results' }) + expect(button).toBeTruthy() + }) + + it('hides button when hasNextPage is false', () => { + const mockOnLoadMore = vi.fn() + const { container } = render( + + ) + + const button = screen.queryByRole('button', { name: 'Load more results' }) + expect(button).toBeNull() + expect(container.firstChild).toBeNull() + }) + + it('shows spinner when isFetchingNextPage is true', () => { + const mockOnLoadMore = vi.fn() + render( + + ) + + const button = screen.getByRole('button', { name: 'Load more results' }) + expect(button).toBeTruthy() + + // Check that the spinner SVG is present + const spinner = button.querySelector('svg') + expect(spinner).toBeTruthy() + expect(spinner?.className).toContain('animate-spin') + + // Check that "Loading..." text is shown + expect(screen.getByText('Loading...')).toBeTruthy() + }) + + it('shows "Load More" text when not fetching', () => { + const mockOnLoadMore = vi.fn() + render( + + ) + + expect(screen.getByText('Load More')).toBeTruthy() + }) + + it('disables button when isFetchingNextPage is true', () => { + const mockOnLoadMore = vi.fn() + render( + + ) + + const button = screen.getByRole('button', { name: 'Load more results' }) + expect(button).toBeDisabled() + }) + + it('calls onLoadMore when clicked', () => { + const mockOnLoadMore = vi.fn() + + render( + + ) + + const button = screen.getByRole('button', { name: 'Load more results' }) + fireEvent.click(button) + + expect(mockOnLoadMore).toHaveBeenCalledTimes(1) + }) + + it('does not call onLoadMore when disabled by isFetchingNextPage', () => { + const mockOnLoadMore = vi.fn() + + render( + + ) + + const button = screen.getByRole('button', { name: 'Load more results' }) + fireEvent.click(button) + + expect(mockOnLoadMore).not.toHaveBeenCalled() + }) + + it('has correct aria-label for accessibility', () => { + const mockOnLoadMore = vi.fn() + render( + + ) + + const button = screen.getByRole('button', { name: 'Load more results' }) + expect(button).toHaveAttribute('aria-label', 'Load more results') + }) + + it('respects disabled prop', () => { + const mockOnLoadMore = vi.fn() + + render( + + ) + + const button = screen.getByRole('button', { name: 'Load more results' }) + expect(button).toBeDisabled() + + fireEvent.click(button) + expect(mockOnLoadMore).not.toHaveBeenCalled() + }) +}) diff --git a/src/components/ui/LoadMoreButton.tsx b/src/components/ui/LoadMoreButton.tsx new file mode 100644 index 0000000..17e8b92 --- /dev/null +++ b/src/components/ui/LoadMoreButton.tsx @@ -0,0 +1,52 @@ +// ─── Reusable: LoadMoreButton ───────────────────────────────────────────────── + +interface LoadMoreButtonProps { + hasNextPage: boolean; + isFetchingNextPage: boolean; + onLoadMore: () => void; + disabled?: boolean; +} + +export function LoadMoreButton({ + hasNextPage, + isFetchingNextPage, + onLoadMore, + disabled = false, +}: LoadMoreButtonProps) { + // Hide button when there's no next page + if (!hasNextPage) { + return null; + } + + return ( + + ); +}