jest.mock('@/lib/cardIndex', () => ({
CARD_INDEX: [
{
clean_name: 'yasuo',
image_url: 'https://example.com/yasuo.png',
set_label: 'Origins',
riftbound_id: 'ori-042-219',
},
{
clean_name: 'jhin virtuoso',
image_url: 'https://example.com/jhin.png',
set_label: 'Unleashed',
riftbound_id: 'unl-181-219',
},
],
}));
import { render, screen } from '@testing-library/react';
import { ChatMessage } from './ChatMessage';
import type { Message } from '@/store/useQueryStore';
const baseMessage: Message = {
id: 'test-1',
question: 'Can I attack with a tapped unit?',
answer: null,
citations: [],
confidence: null,
latencyMs: null,
loading: false,
error: null,
};
describe('ChatMessage', () => {
it('renders the user question', () => {
render();
expect(screen.getByText('Can I attack with a tapped unit?')).toBeInTheDocument();
});
it('renders judge answer when available', () => {
const msg: Message = { ...baseMessage, answer: 'No, tapped units cannot attack.' };
render();
expect(screen.getByText(/tapped units cannot attack/)).toBeInTheDocument();
});
it('renders loading skeleton when loading is true', () => {
const msg: Message = { ...baseMessage, loading: true };
const { container } = render();
expect(container.firstChild).toBeTruthy();
});
it('shows the Judge label', () => {
render();
expect(screen.getByText('Judge')).toBeInTheDocument();
});
it('renders a system notice instead of the judge bubble on error', () => {
const msg: Message = { ...baseMessage, error: { type: 'timeout', message: '' } };
render();
expect(screen.getByText('System Notice')).toBeInTheDocument();
// the fault must NOT be attributed to the judge
expect(screen.queryByText('Judge')).toBeNull();
});
it('shows citations section when answer and citations exist', () => {
const msg: Message = {
...baseMessage,
answer: 'Yes.',
citations: [{
section: '4.1',
source_type: 'rule',
content_preview: 'Units may attack...',
similarity: 0.9,
}],
};
render();
// One citation -> the popover trigger reads "1 source" (singular).
expect(screen.getByText(/source/i)).toBeInTheDocument();
});
it('does not show citations section when empty', () => {
const msg: Message = { ...baseMessage, answer: 'Yes.', citations: [] };
render();
expect(screen.queryByText(/Sources/i)).toBeNull();
});
it('renders #hunt as a KeywordBadge in user bubble (not raw #hunt text)', () => {
const msg: Message = { ...baseMessage, question: 'Can #hunt trigger twice?' };
render();
expect(screen.queryByText('#hunt')).toBeNull();
expect(screen.getByText('HUNT')).toBeInTheDocument();
});
it('keeps unrecognized #mentions as plain text', () => {
const msg: Message = { ...baseMessage, question: 'Can #unknownkeyword appear?' };
render();
expect(screen.getByText(/#unknownkeyword/)).toBeInTheDocument();
});
it('renders @yasuo as a CardChip wrapped in a hover trigger', () => {
const msg: Message = { ...baseMessage, question: 'Can @yasuo attack?' };
render();
expect(screen.queryByText('@yasuo')).toBeNull();
expect(screen.getByText('YASUO')).toBeInTheDocument();
const trigger = document.querySelector('[data-slot="hover-card-trigger"]');
expect(trigger).not.toBeNull();
expect(trigger?.textContent).toContain('YASUO');
});
it('renders a hyphenated slug @jhin-virtuoso showing the pretty card name', () => {
const msg: Message = { ...baseMessage, question: 'Does @jhin-virtuoso work?' };
render();
expect(screen.queryByText('@jhin-virtuoso')).toBeNull();
expect(screen.getByText('JHIN VIRTUOSO')).toBeInTheDocument();
});
it('resolves a bare prefix @jhin to the full card name (first-wins)', () => {
const msg: Message = { ...baseMessage, question: 'Does @jhin work?' };
render();
expect(screen.getByText('JHIN VIRTUOSO')).toBeInTheDocument();
});
it('does not cross sigils: #yasuo (a card, not a keyword) stays plain text', () => {
const msg: Message = { ...baseMessage, question: 'Is #yasuo strong?' };
render();
expect(screen.getByText(/#yasuo/)).toBeInTheDocument();
expect(document.querySelector('[data-slot="hover-card-trigger"]')).toBeNull();
});
it('does not cross sigils: @hunt (a keyword, not a card) stays plain text', () => {
const msg: Message = { ...baseMessage, question: 'Does @hunt resolve?' };
render();
expect(screen.getByText(/@hunt/)).toBeInTheDocument();
expect(screen.queryByText('HUNT')).toBeNull();
});
});