import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import '../i18n'
import { InventoryPanel } from './InventoryPanel'
function mockFetch(handlers: {
items?: unknown
itemsStatus?: number
catalog?: unknown
catalogStatus?: number
purchaseStatus?: number
}) {
vi.stubGlobal(
'fetch',
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
if (url.includes('/api/inventory/items')) {
return new Response(JSON.stringify(handlers.items ?? { items: [] }), {
status: handlers.itemsStatus ?? 200,
headers: { 'Content-Type': 'application/json' },
})
}
if (url.includes('/api/inventory/catalog')) {
if (handlers.catalogStatus && handlers.catalogStatus !== 200) {
return new Response('not found', { status: handlers.catalogStatus })
}
return new Response(JSON.stringify(handlers.catalog ?? { unlocks: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}
if (url.includes('/api/inventory/unlocks/') && init?.method === 'POST') {
return new Response('{}', { status: handlers.purchaseStatus ?? 200 })
}
return new Response('not found', { status: 404 })
}),
)
}
describe('InventoryPanel', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
afterEach(() => {
vi.unstubAllGlobals()
})
it('shows the essence balance prominently', async () => {
mockFetch({ catalog: { unlocks: [{ unlock_key: 'listening_tool', price: 40 }] } })
render()
expect(screen.getByTestId('essence-value').textContent).toBe('128')
await screen.findByRole('button', { name: /buy/i }) // let pending fetches settle
})
it('lists owned unlocks with an [OWNED] tag and hides them from the buy catalog', async () => {
mockFetch({ catalog: { unlocks: [{ unlock_key: 'listening_tool', price: 40 }] } })
render()
expect(await screen.findByText('listening_tool')).toBeTruthy()
expect(screen.getByText('[OWNED]')).toBeTruthy()
expect(screen.queryByRole('button', { name: /buy/i })).toBeNull()
})
it('disables the buy button when the user cannot afford the unlock', async () => {
mockFetch({ catalog: { unlocks: [{ unlock_key: 'listening_tool', price: 40 }] } })
render()
const btn = await screen.findByRole('button', { name: /buy/i })
await waitFor(() => expect(btn).toHaveProperty('disabled', true))
})
it('enables the buy button when the user can afford the unlock', async () => {
mockFetch({ catalog: { unlocks: [{ unlock_key: 'listening_tool', price: 40 }] } })
render()
const btn = await screen.findByRole('button', { name: /buy/i })
await waitFor(() => expect(btn).toHaveProperty('disabled', false))
})
it('falls back to a flagged estimated price when the catalog endpoint is unavailable', async () => {
mockFetch({ catalogStatus: 404 })
render()
await waitFor(() => expect(screen.getByText(/\(est\.\)/)).toBeTruthy())
const btn = screen.getByRole('button', { name: /buy/i })
expect(btn).toHaveProperty('disabled', false) // 100 essence affords the 40 fallback price
})
it('purchasing successfully moves the unlock into the owned list and deducts essence', async () => {
mockFetch({
catalog: { unlocks: [{ unlock_key: 'listening_tool', price: 40 }] },
purchaseStatus: 200,
})
const user = userEvent.setup()
render()
const btn = await screen.findByRole('button', { name: /buy/i })
await waitFor(() => expect(btn).toHaveProperty('disabled', false))
await user.click(btn)
await waitFor(() => expect(screen.getByTestId('essence-value').textContent).toBe('60'))
expect(screen.getByText('[OWNED]')).toBeTruthy()
expect(screen.queryByRole('button', { name: /buy/i })).toBeNull()
})
it('shows an insufficient-funds message on a 402 without changing essence', async () => {
mockFetch({
catalog: { unlocks: [{ unlock_key: 'listening_tool', price: 40 }] },
purchaseStatus: 402,
})
const user = userEvent.setup()
render()
const btn = await screen.findByRole('button', { name: /buy/i })
await waitFor(() => expect(btn).toHaveProperty('disabled', false))
await user.click(btn)
await screen.findByRole('alert')
expect(screen.getByTestId('essence-value').textContent).toBe('100')
})
it('renders items with a rarity data attribute when payload carries a known rarity', async () => {
mockFetch({
items: {
items: [
{
item_type: 'trinket',
item_key: 'silver-locket',
payload: { rarity: 'rare' },
obtained_at: '2026-01-01T00:00:00Z',
},
],
},
})
render()
const key = await screen.findByText('silver-locket')
const card = key.closest('[data-rarity]')
expect(card?.getAttribute('data-rarity')).toBe('rare')
})
})