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') }) })