import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import App from './App'
import { makeDataset, makeGame, makeState } from './test/factories'
describe('App', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('renders the planner after data loads', async () => {
const ds = makeDataset([makeGame({ app_id: 1, name: 'Alpha' })])
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: true, json: async () => ds }),
)
render()
expect(screen.getByText(/Loading your backlog/i)).toBeInTheDocument()
await waitFor(() =>
expect(
screen.getByRole('heading', { name: 'Backlog Completion Planner' }),
).toBeInTheDocument(),
)
expect(screen.getByText(/CLI default qualifies/i)).toBeInTheDocument()
})
it('shows an error when the API call fails', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: false, status: 500, statusText: 'Server Error' }),
)
render()
await waitFor(() =>
expect(screen.getByText(/Could not load data/i)).toBeInTheDocument(),
)
})
it('handles a non-Error rejection', async () => {
vi.stubGlobal('fetch', vi.fn().mockRejectedValue('network down'))
render()
await waitFor(() =>
expect(screen.getByText(/network down/i)).toBeInTheDocument(),
)
})
it('recomputes scope when basis changes and a game is excluded', async () => {
const ds = makeDataset(
[makeGame({ app_id: 1, name: 'Alpha' }), makeGame({ app_id: 2, name: 'Beta' })],
{
state: makeState({
current_game_name: 'Hollow Knight',
enforcement_started_at: '2026-03-04T00:00:00+00:00',
pace_games_per_day: 0.9,
}),
},
)
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: true, json: async () => ds }),
)
const user = userEvent.setup()
render()
await waitFor(() =>
expect(
screen.getByRole('heading', { name: 'Backlog Completion Planner' }),
).toBeInTheDocument(),
)
// The current game appears in the header (covers the conditional branch).
expect(screen.getByText(/Hollow Knight/)).toBeInTheDocument()
expect(document.querySelector('.big')?.textContent).toBe('2')
// Switching basis promotes the Rush card to active.
await user.click(screen.getByRole('button', { name: 'Rush' }))
expect(document.querySelector('.card.active .card-title')?.textContent).toBe('Rush')
// Excluding a game drops the in-scope count.
await user.click(within(screen.getByRole('table')).getAllByRole('checkbox')[0])
expect(document.querySelector('.big')?.textContent).toBe('1')
// Re-including it restores the count (covers the toggle-off branch).
await user.click(within(screen.getByRole('table')).getAllByRole('checkbox')[0])
expect(document.querySelector('.big')?.textContent).toBe('2')
// Searching narrows the table (covers the search handler).
await user.type(screen.getByPlaceholderText(/Search games/i), 'Alpha')
expect(within(screen.getByRole('table')).queryByText('Beta')).toBeNull()
// Reset restores the full scope (covers the reset handler).
await user.click(screen.getByRole('button', { name: /Reset to CLI defaults/i }))
expect(document.querySelector('.big')?.textContent).toBe('2')
})
})