import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import { MapCharacterPanel } from '../../src/components/characters/MapCharacterPanel'; const { mockGetShopItems, mockGetGoldBalance, mockSendNpcMessage, mockGetNpcChatSession, mockStartNpcChat, } = vi.hoisted(() => ({ mockGetShopItems: vi.fn().mockResolvedValue([]), mockGetGoldBalance: vi.fn().mockResolvedValue(300), mockSendNpcMessage: vi.fn().mockResolvedValue({ messages: [], balance: { gold: 300 }, negotiation: { status: 'active' }, shop_items: [], }), mockGetNpcChatSession: vi.fn().mockResolvedValue({ messages: [] }), mockStartNpcChat: vi.fn().mockResolvedValue({ messages: [] }), })); const mockUseCharacter = vi.fn(); vi.mock('../../src/store/characterStore', () => ({ useCharacter: () => mockUseCharacter(), })); vi.mock('../../src/services/api', () => ({ apiService: { getShopItems: mockGetShopItems, getGoldBalance: mockGetGoldBalance, sendNpcMessage: mockSendNpcMessage, getNpcChatSession: mockGetNpcChatSession, startNpcChat: mockStartNpcChat, }, })); describe('MapCharacterPanel rich chat formatting', () => { beforeEach(() => { vi.clearAllMocks(); mockGetShopItems.mockResolvedValue([]); mockGetGoldBalance.mockResolvedValue(300); mockSendNpcMessage.mockResolvedValue({ messages: [], balance: { gold: 300 }, negotiation: { status: 'active' }, shop_items: [], }); mockGetNpcChatSession.mockResolvedValue({ messages: [] }); mockStartNpcChat.mockResolvedValue({ messages: [] }); mockUseCharacter.mockReturnValue({ activeCharacter: 'frodo', setActiveCharacter: vi.fn(), isChatLoading: false, chatMessages: [ { role: 'assistant', content: "- **Elven Cloak** — ask **88 Gold**\n- **Ranger's Rope** — ask **40 Gold**", format: 'markdown', type: 'assistant', }, ], setChatMessages: vi.fn(), setChatLoading: vi.fn(), }); }); it('renders bold prices and list-like bargaining messages', () => { render(); expect(screen.getByText('Elven Cloak', { selector: 'strong' })).toBeTruthy(); expect(screen.getByText('88 Gold', { selector: 'strong' })).toBeTruthy(); expect(screen.getByText("Ranger's Rope", { selector: 'strong' })).toBeTruthy(); }); it('loads the wares catalog directly instead of opening with generic chat opener', async () => { const setChatMessages = vi.fn(); const catalogMessages = [ { role: 'assistant', content: 'Available wares to bargain for:\n- **Shire Herb Satchel** — ask **120 Gold** (id: #2)', format: 'markdown', type: 'assistant', metadata: { kind: 'shop-catalog' }, }, ]; mockUseCharacter.mockReturnValue({ activeCharacter: 'sam', setActiveCharacter: vi.fn(), isChatLoading: false, chatMessages: [], setChatMessages, setChatLoading: vi.fn(), }); mockGetNpcChatSession.mockResolvedValue({ messages: [] }); mockSendNpcMessage.mockResolvedValue({ messages: catalogMessages, balance: { gold: 300 }, negotiation: { status: 'catalog' }, shop_items: [], }); render(); await waitFor(() => { expect(mockSendNpcMessage).toHaveBeenCalledWith('sam', 'shop'); }); expect(mockStartNpcChat).not.toHaveBeenCalled(); expect(setChatMessages).toHaveBeenCalledWith(catalogMessages); }); });