Files
kotobane/__tests__/components/LoadMoreButton.test.tsx
T

146 lines
4.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import LoadMoreButton from '@/components/article/LoadMoreButton'
process.env.NEXT_PUBLIC_DIRECTUS_URL = 'https://cms.achmad.dev'
process.env.NEXT_PUBLIC_DIRECTUS_TOKEN = 'test-token'
const mockFetch = vi.fn()
global.fetch = mockFetch
beforeEach(() => {
vi.resetAllMocks()
mockFetch.mockResolvedValue({
json: () => Promise.resolve({ data: [] }),
})
})
describe('LoadMoreButton', () => {
it('renders the Load more button by default', () => {
render(<LoadMoreButton categorySlug="games" initialCount={12} />)
expect(screen.getByText('Load more')).toBeInTheDocument()
})
it('hides the button when hasMore is false', () => {
render(<LoadMoreButton categorySlug="games" initialCount={12} hasMore={false} />)
expect(screen.queryByText('Load more')).not.toBeInTheDocument()
})
it('shows loading state while fetching', async () => {
mockFetch.mockImplementation(
() =>
new Promise((resolve) =>
setTimeout(
() =>
resolve({
json: () => Promise.resolve({ data: [] }),
}),
100,
),
),
)
render(<LoadMoreButton categorySlug="games" initialCount={12} />)
fireEvent.click(screen.getByText('Load more'))
expect(await screen.findByText('Loading…')).toBeInTheDocument()
})
it('fetches articles with correct parameters', async () => {
render(<LoadMoreButton categorySlug="games" initialCount={12} />)
fireEvent.click(screen.getByText('Load more'))
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledTimes(1)
})
const url = mockFetch.mock.calls[0][0] as string
expect(url).toContain('/items/articles')
expect(url).toContain('games')
expect(url).toContain('offset=12')
expect(url).toContain('limit=12')
expect(url).toContain('access_token=test-token')
})
it('appends fetched articles to the display', async () => {
mockFetch.mockResolvedValue({
json: () =>
Promise.resolve({
data: [
{
id: '101',
title: 'Loaded Article',
slug: 'loaded-article',
excerpt: null,
featured_image: null,
published_at: null,
date_created: '2026-05-31T00:00:00Z',
is_featured: false,
category: { id: '1', name: 'Games', slug: 'games' },
},
],
}),
})
render(<LoadMoreButton categorySlug="games" initialCount={12} />)
fireEvent.click(screen.getByText('Load more'))
await waitFor(() => {
expect(screen.getByText('Loaded Article')).toBeInTheDocument()
})
})
it('hides button after last page when fewer results than limit are returned', async () => {
mockFetch.mockResolvedValue({
json: () => Promise.resolve({ data: [] }),
})
render(<LoadMoreButton categorySlug="games" initialCount={12} />)
expect(screen.getByText('Load more')).toBeInTheDocument()
fireEvent.click(screen.getByText('Load more'))
await waitFor(() => {
expect(screen.queryByText('Load more')).not.toBeInTheDocument()
})
})
it('keeps button visible when exactly limit articles are returned', async () => {
const articles = Array.from({ length: 12 }, (_, i) => ({
id: String(200 + i),
title: `Article ${i}`,
slug: `article-${i}`,
excerpt: null,
featured_image: null,
published_at: null,
date_created: '2026-05-31T00:00:00Z',
is_featured: false,
category: { id: '1', name: 'Games', slug: 'games' },
}))
mockFetch.mockResolvedValue({
json: () => Promise.resolve({ data: articles }),
})
render(<LoadMoreButton categorySlug="games" initialCount={12} />)
fireEvent.click(screen.getByText('Load more'))
await waitFor(() => {
expect(screen.getByText('Load more')).toBeInTheDocument()
})
})
it('calls the correct Directus API URL', async () => {
render(<LoadMoreButton categorySlug="vtubers" initialCount={14} />)
fireEvent.click(screen.getByText('Load more'))
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledTimes(1)
})
const url = mockFetch.mock.calls[0][0] as string
expect(url).toContain('vtubers')
expect(url).toContain('offset=14')
})
})