import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MessageBubble from '../../../../src/renderer/components/chat/MessageBubble';
import { useAgentStore } from '../../../../src/renderer/stores/agentStore';
import { useConfigStore } from '../../../../src/renderer/stores/configStore';
jest.mock('@hashgraphonline/standards-sdk', () => ({
Logger: jest.fn().mockImplementation(() => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
})),
TransactionParser: jest.fn(),
HCSMessage: jest.fn(),
}));
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: jest.fn().mockResolvedValue(undefined),
},
writable: true,
});
jest.mock('../../../../src/renderer/stores/configStore', () => ({
useConfigStore: {
getState: () => ({
config: {
hedera: {
network: 'testnet',
accountId: '0.0.12345',
},
advanced: {
operationalMode: 'autonomous',
},
},
}),
subscribe: jest.fn(),
destroy: jest.fn(),
},
}));
jest.mock('../../../../src/renderer/stores/agentStore');
jest.mock('../../../../src/renderer/components/ui/Typography', () => ({
__esModule: true,
default: ({ children, variant, className, onClick }: any) => (
{children}
),
}));
jest.mock('../../../../src/renderer/components/ui/Logo', () => ({
__esModule: true,
default: () => Logo
,
}));
jest.mock('../../../../src/renderer/components/ui/UserProfileImage', () => ({
__esModule: true,
default: ({ accountId, network }: any) => (
Profile Image
),
}));
jest.mock('../../../../src/renderer/components/chat/AttachmentDisplay', () => ({
__esModule: true,
default: ({ attachments }: any) => (
{attachments?.map((att: any, i: number) => (
{att.name}
))}
),
}));
jest.mock(
'../../../../src/renderer/components/chat/TransactionApproval',
() => ({
TransactionApprovalButton: ({ messageId, onApprove, onReject }: any) => (
),
})
);
jest.mock('../../../../src/renderer/components/ui/CodeBlock', () => ({
CodeBlock: ({ children, language }: any) => (
{children}
),
}));
jest.mock('../../../../src/renderer/components/chat/FormMessageBubble', () => ({
FormMessageBubble: ({ formMessage }: any) => (
Form: {formMessage?.formConfig?.title}
),
}));
jest.mock(
'../../../../src/renderer/components/chat/HashLinkBlockRenderer',
() => ({
__esModule: true,
default: ({ content }: any) => (
{content}
),
})
);
jest.mock('../../../../src/renderer/utils/markdownProcessor', () => ({
processMarkdown: jest.fn((text) => `${text}
`),
}));
jest.mock('../../../../src/renderer/lib/utils', () => ({
cn: jest.fn((...classes) => classes.filter(Boolean).join(' ')),
}));
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => {children}
,
},
}));
jest.mock('react-icons/fi', () => ({
FiHash: () => ,
FiClock: () => ,
FiCopy: () => ,
FiCheck: () => ,
FiMaximize2: () => ,
FiX: () => ,
FiImage: () => ,
}));
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: jest.fn().mockResolvedValue(undefined),
},
writable: true,
});
describe('MessageBubble', () => {
const mockAgentStore = {
approveTransaction: jest.fn(),
rejectTransaction: jest.fn(),
findFormMessage: jest.fn(),
};
const mockConfigStore = {
getState: () => ({
config: {
hedera: {
network: 'testnet',
accountId: '0.0.12345',
},
advanced: {
operationalMode: 'autonomous',
},
},
}),
subscribe: jest.fn(),
destroy: jest.fn(),
};
const mockMessage: any = {
id: 'message-1',
role: 'user',
content: 'Hello, this is a test message!',
timestamp: new Date('2024-01-01T12:00:00Z'),
metadata: {},
};
const mockUserProfile = {
accountId: '0.0.12345',
name: 'Test User',
network: 'testnet',
};
beforeEach(() => {
jest.clearAllMocks();
(useAgentStore as jest.MockedFunction).mockReturnValue(mockAgentStore);
});
describe('Basic Rendering', () => {
test('should render user message correctly', () => {
render(
);
expect(
screen.getByText('Hello, this is a test message!')
).toBeInTheDocument();
expect(screen.getByTestId('user-profile-image')).toBeInTheDocument();
});
test('should render assistant message correctly', () => {
const assistantMessage = {
...mockMessage,
role: 'assistant',
content: 'Hello! How can I help you?',
};
render(
);
expect(
screen.getByText('Hello! How can I help you?')
).toBeInTheDocument();
expect(screen.getByTestId('logo')).toBeInTheDocument();
});
test('should render system message correctly', () => {
const systemMessage = {
...mockMessage,
role: 'system',
content: 'System notification',
};
render(
);
expect(screen.getByText('System notification')).toBeInTheDocument();
});
});
describe('Message Content Processing', () => {
test('should process markdown content', () => {
const markdownMessage = {
...mockMessage,
content: '**Bold text** and *italic text*',
};
render(
);
expect(
screen.getByText('**Bold text** and *italic text*')
).toBeInTheDocument();
});
test('should handle empty message content', () => {
const emptyMessage = {
...mockMessage,
content: '',
};
render(
);
expect(screen.getByTestId('user-profile-image')).toBeInTheDocument();
});
test('should handle message with code blocks', () => {
const codeMessage = {
...mockMessage,
content: '```javascript\nconsole.log("Hello");\n```',
};
render(
);
expect(screen.getByTestId('code-block')).toBeInTheDocument();
});
});
describe('Attachments', () => {
test('should render attachments when present', () => {
const messageWithAttachments = {
...mockMessage,
metadata: {
attachments: [
{
name: 'test.txt',
data: 'file content',
type: 'text/plain',
size: 1024,
},
],
},
};
render(
);
expect(screen.getByTestId('attachment-display')).toBeInTheDocument();
expect(screen.getByTestId('attachment-0')).toBeInTheDocument();
expect(screen.getByText('test.txt')).toBeInTheDocument();
});
test('should handle multiple attachments', () => {
const messageWithMultipleAttachments = {
...mockMessage,
metadata: {
attachments: [
{
name: 'file1.txt',
data: 'content1',
type: 'text/plain',
size: 1024,
},
{
name: 'file2.jpg',
data: 'image data',
type: 'image/jpeg',
size: 2048,
},
],
},
};
render(
);
expect(screen.getByTestId('attachment-0')).toBeInTheDocument();
expect(screen.getByTestId('attachment-1')).toBeInTheDocument();
expect(screen.getByText('file1.txt')).toBeInTheDocument();
expect(screen.getByText('file2.jpg')).toBeInTheDocument();
});
});
describe('Transaction Messages', () => {
test('should render transaction approval button for pending transactions', () => {
const transactionMessage = {
...mockMessage,
metadata: {
transactionBytes: 'SGVsbG8gV29ybGQ=',
pendingApproval: true,
},
};
render(
);
expect(
screen.getByTestId('transaction-approval-button')
).toBeInTheDocument();
expect(screen.getByTestId('approve-btn')).toBeInTheDocument();
expect(screen.getByTestId('reject-btn')).toBeInTheDocument();
});
test('should handle transaction approval', async () => {
const user = userEvent.setup();
const transactionMessage = {
...mockMessage,
metadata: {
transactionBytes: 'SGVsbG8gV29ybGQ=',
pendingApproval: true,
},
};
render(
);
const approveButton = screen.getByTestId('approve-btn');
await user.click(approveButton);
expect(mockAgentStore.approveTransaction).toHaveBeenCalledWith(
'message-1'
);
});
test('should handle transaction rejection', async () => {
const user = userEvent.setup();
const transactionMessage = {
...mockMessage,
metadata: {
transactionBytes: 'SGVsbG8gV29ybGQ=',
pendingApproval: true,
},
};
render(
);
const rejectButton = screen.getByTestId('reject-btn');
await user.click(rejectButton);
expect(mockAgentStore.rejectTransaction).toHaveBeenCalledWith(
'message-1'
);
});
});
describe('Form Messages', () => {
test('should render form message bubble when form data is present', () => {
const formMessage = {
type: 'form',
id: 'form-1',
formConfig: {
title: 'Test Form',
fields: [
{
name: 'field1',
label: 'Field 1',
type: 'text',
required: true,
},
],
},
originalPrompt: 'Please fill this form',
toolName: 'test-tool',
};
const messageWithForm = {
...mockMessage,
metadata: {
formMessage: formMessage,
},
};
render(
);
expect(screen.getByTestId('form-message-bubble')).toBeInTheDocument();
expect(screen.getByText('Form: Test Form')).toBeInTheDocument();
});
});
describe('HCS-10 Messages', () => {
test('should render HCS-10 message with agent name', () => {
render(
);
expect(screen.getByText('Test Agent')).toBeInTheDocument();
});
test('should handle HCS-10 user identification', () => {
const hcs10Message = {
...mockMessage,
metadata: {
operatorId: '0.0.12345',
},
};
render(
);
expect(screen.getByTestId('user-profile-image')).toBeInTheDocument();
});
});
describe('Copy Functionality', () => {
test('should copy message content to clipboard', async () => {
const user = userEvent.setup();
render(
);
const copyButtons = screen.getAllByTestId('fi-copy');
if (copyButtons.length > 0) {
await user.click(copyButtons[0]);
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
'Hello, this is a test message!'
);
}
});
test('should show copy success feedback', async () => {
const user = userEvent.setup();
render(
);
const copyButtons = screen.getAllByTestId('fi-copy');
if (copyButtons.length > 0) {
await user.click(copyButtons[0]);
expect(navigator.clipboard.writeText).toHaveBeenCalled();
}
});
});
describe('Timestamp Display', () => {
test('should display message timestamp', () => {
render(
);
expect(screen.getByTestId('fi-clock')).toBeInTheDocument();
});
test('should handle malformed timestamp gracefully', () => {
const messageWithBadTimestamp = {
...mockMessage,
timestamp: 'invalid-date' as any,
};
expect(() => {
render(
);
}).not.toThrow();
});
});
describe('Agent Profile Interaction', () => {
test('should call onAgentProfileClick when agent profile is clicked', async () => {
const user = userEvent.setup();
const mockOnAgentProfileClick = jest.fn();
render(
);
expect(mockOnAgentProfileClick).not.toHaveBeenCalled();
});
});
describe('Message Types and Roles', () => {
test('should handle different message roles', () => {
const roles = ['user', 'assistant', 'system'];
roles.forEach((role) => {
const messageByRole = {
...mockMessage,
role: role as 'user' | 'assistant' | 'system',
};
const { unmount } = render(
);
expect(
screen.getByText('Hello, this is a test message!')
).toBeInTheDocument();
unmount();
});
});
test('should handle messages with special characters', () => {
const specialMessage = {
...mockMessage,
content:
'Message with and & entities',
};
render(
);
expect(
screen.getByText(
'Message with and & entities'
)
).toBeInTheDocument();
});
});
describe('Error Handling', () => {
test('should handle missing user profile gracefully', () => {
render();
expect(
screen.getByText('Hello, this is a test message!')
).toBeInTheDocument();
});
test('should handle undefined metadata', () => {
const messageWithoutMetadata = {
...mockMessage,
metadata: undefined,
};
render(
);
expect(
screen.getByText('Hello, this is a test message!')
).toBeInTheDocument();
});
});
describe('Accessibility', () => {
test('should have proper ARIA attributes', () => {
render(
);
const messageElement = screen.getByText('Hello, this is a test message!');
expect(messageElement).toBeInTheDocument();
});
test('should support keyboard navigation', () => {
render(
);
expect(
screen.getByText('Hello, this is a test message!')
).toBeInTheDocument();
});
});
describe('Performance', () => {
test('should render large messages efficiently', () => {
const largeMessage = {
...mockMessage,
content: 'A'.repeat(10000), // Large content
};
const startTime = Date.now();
render(
);
const renderTime = Date.now() - startTime;
expect(renderTime).toBeLessThan(1000); // Should render within reasonable time
});
test('should handle frequent re-renders', () => {
const { rerender } = render(
);
rerender(
);
expect(
screen.getByText('Hello, this is a test message!')
).toBeInTheDocument();
});
});
describe('Integration Scenarios', () => {
test('should handle complete message workflow', () => {
const complexMessage = {
...mockMessage,
content: 'Complex message with **markdown** and `code`',
metadata: {
attachments: [
{
name: 'document.pdf',
data: 'pdf data',
type: 'application/pdf',
size: 1024000,
},
],
transactionBytes: 'SGVsbG8gV29ybGQ=',
pendingApproval: true,
formMessage: {
type: 'form',
id: 'form-1',
formConfig: {
title: 'Test Form',
fields: [],
},
originalPrompt: 'Test prompt',
toolName: 'test-tool',
},
},
};
render(
);
expect(
screen.getByText('Complex message with **markdown** and `code`')
).toBeInTheDocument();
expect(screen.getByTestId('attachment-display')).toBeInTheDocument();
expect(
screen.getByTestId('transaction-approval-button')
).toBeInTheDocument();
expect(screen.getByTestId('form-message-bubble')).toBeInTheDocument();
});
test('should handle message updates', () => {
const { rerender } = render(
);
const updatedMessage = {
...mockMessage,
content: 'Updated message content',
};
rerender(
);
expect(screen.getByText('Updated message content')).toBeInTheDocument();
});
});
});