import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { AgentMessageText, BubbleMarkdown, TableCellMarkdown } from './AgentMessageBubble'; const mocks = vi.hoisted(() => ({ openUrl: vi.fn(), openWorkspacePath: vi.fn() })); vi.mock('../../../utils/openUrl', () => ({ openUrl: mocks.openUrl })); vi.mock('../../../utils/tauriCommands/workspacePaths', () => ({ openWorkspacePath: mocks.openWorkspacePath, })); describe('AgentMessageBubble markdown links', () => { beforeEach(() => { mocks.openUrl.mockReset(); mocks.openUrl.mockResolvedValue(undefined); mocks.openWorkspacePath.mockReset(); mocks.openWorkspacePath.mockResolvedValue(undefined); }); test('opens allowed external links through the OS URL handler', async () => { render(); await userEvent.click(screen.getByRole('link', { name: 'docs' })); await waitFor(() => expect(mocks.openUrl).toHaveBeenCalledWith('https://example.com/docs')); expect(mocks.openWorkspacePath).not.toHaveBeenCalled(); }); test('opens workspace links through the Tauri workspace path command', async () => { render(); await userEvent.click(screen.getByRole('link', { name: 'summary' })); await waitFor(() => expect(mocks.openWorkspacePath).toHaveBeenCalledWith('memory_tree/content/summary.md') ); expect(mocks.openUrl).not.toHaveBeenCalled(); }); test('logs workspace link open failures for diagnostics', async () => { const error = new Error('missing file'); const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); mocks.openWorkspacePath.mockRejectedValueOnce(error); try { render(); await userEvent.click(screen.getByRole('link', { name: 'summary' })); await waitFor(() => expect(consoleError).toHaveBeenCalledWith('workspace open failed:', error) ); } finally { consoleError.mockRestore(); } }); test('uses the same workspace link handling inside table cells', async () => { render(); await userEvent.click(screen.getByRole('link', { name: 'note' })); await waitFor(() => expect(mocks.openWorkspacePath).toHaveBeenCalledWith('docs/note.md')); expect(mocks.openUrl).not.toHaveBeenCalled(); }); test('does not open raw file links from markdown', async () => { render(); await userEvent.click(screen.getByText('secret')); expect(mocks.openUrl).not.toHaveBeenCalled(); expect(mocks.openWorkspacePath).not.toHaveBeenCalled(); }); }); describe('BubbleMarkdown math rendering', () => { test('renders GFM markdown tables when they appear inside regular bubble markdown', () => { render( ); expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'PR' })).toBeInTheDocument(); expect(screen.getByRole('cell', { name: '#3784' })).toBeInTheDocument(); }); test('renders \\[ ... \\] block math (raw delimiters consumed, math visible)', () => { const { container } = render(); const text = container.textContent ?? ''; expect(text).not.toContain('\\['); expect(text).not.toContain('\\]'); expect(text).toContain('x'); expect(text).toContain('y'); expect(text).toContain('z'); }); test('renders inline \\( ... \\) math (raw delimiters consumed, math visible)', () => { const { container } = render(); const text = container.textContent ?? ''; expect(text).not.toContain('\\('); expect(text).not.toContain('\\)'); expect(text).toContain('value'); expect(text).toContain('here'); expect(text).toContain('a'); expect(text).toContain('b'); }); test('renders bare bracket vmatrix block (math rendered, not raw text)', () => { const { container } = render( ); const text = container.textContent ?? ''; // KaTeX renders visible glyphs (∣ for vmatrix bars) — confirm rendering happened. expect(text).toContain('∣'); expect(text).toContain('1'); expect(text).toContain('4'); }); test('does NOT treat currency mentions as math', () => { const { container } = render(); expect(container.textContent).toContain('$10'); expect(container.textContent).toContain('$20'); }); }); describe('AgentMessageText', () => { test('renders openhuman link pills without assistant bubble chrome', () => { render( Appearance'} /> ); expect(screen.getByTestId('agent-message-text')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Appearance/ })).toBeInTheDocument(); }); test('uses the dedicated table renderer in plain text mode', () => { render(); expect(screen.getByTestId('agent-message-text')).toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument(); expect(screen.getByRole('cell', { name: 'OpenHuman' })).toBeInTheDocument(); }); });