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