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