import type { VueWrapper } from '@vue/test-utils'; import { mount } from '@vue/test-utils'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { useChat } from '@n8n/chat/composables'; import { chatEventBus } from '@n8n/chat/event-buses'; import type { ChatMessage } from '@n8n/chat/types'; import MessageActions from '../components/MessageActions.vue'; vi.mock('@n8n/design-system', () => ({ N8nTooltip: { name: 'N8nTooltip', template: '
', }, N8nIcon: { name: 'N8nIcon', template: '
', props: ['icon', 'size'], }, })); vi.mock('@n8n/chat/composables', () => ({ useChat: vi.fn(() => ({ sendMessage: vi.fn(), })), useOptions: () => ({ options: { enableMessageActions: true, }, }), useI18n: () => ({ t: vi.fn(), }), })); vi.mock('@n8n/chat/event-buses', () => ({ chatEventBus: { emit: vi.fn(), }, })); describe('MessageActions', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let wrapper: VueWrapper; const userMessage: ChatMessage = { id: '1', text: 'Hello, world!', sender: 'user', type: 'text', }; beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { if (wrapper) { wrapper.unmount(); } }); it('should render message actions for user messages when enabled', () => { wrapper = mount(MessageActions, { props: { message: userMessage, }, }); expect(wrapper.find('.message-actions').exists()).toBe(true); }); it('should call sendMessage when repost icon is clicked', async () => { const mockSendMessage = vi.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument vi.mocked(useChat).mockReturnValue({ sendMessage: mockSendMessage } as any); wrapper = mount(MessageActions, { props: { message: userMessage, }, }); const repostIcon = wrapper.find('[icon="redo-2"]'); expect(repostIcon.exists()).toBe(true); await repostIcon.trigger('click'); expect(mockSendMessage).toHaveBeenCalledWith('Hello, world!', []); }); it('should emit setInputValue event when copy to input icon is clicked', async () => { wrapper = mount(MessageActions, { props: { message: userMessage, }, }); const copyIcon = wrapper.find('[icon="files"]'); expect(copyIcon.exists()).toBe(true); await copyIcon.trigger('click'); expect(vi.mocked(chatEventBus.emit)).toHaveBeenCalledWith('setInputValue', 'Hello, world!'); }); it('should handle messages with files when reposting', async () => { const mockSendMessage = vi.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument vi.mocked(useChat).mockReturnValue({ sendMessage: mockSendMessage } as any); const messageWithFiles: ChatMessage = { id: '3', text: 'Message with files', sender: 'user', type: 'text', files: [new File(['test'], 'test.txt')], }; wrapper = mount(MessageActions, { props: { message: messageWithFiles, }, }); const repostIcon = wrapper.find('[icon="redo-2"]'); await repostIcon.trigger('click'); expect(mockSendMessage).toHaveBeenCalledWith('Message with files', [expect.any(File)]); }); it('should not repost empty messages', async () => { const mockSendMessage = vi.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument vi.mocked(useChat).mockReturnValue({ sendMessage: mockSendMessage } as any); const emptyMessage: ChatMessage = { id: '4', text: ' ', sender: 'user', type: 'text', }; wrapper = mount(MessageActions, { props: { message: emptyMessage, }, }); const repostIcon = wrapper.find('[icon="redo-2"]'); await repostIcon.trigger('click'); expect(mockSendMessage).not.toHaveBeenCalled(); }); it('should not copy empty messages to input', async () => { const emptyMessage: ChatMessage = { id: '5', text: ' ', sender: 'user', type: 'text', }; wrapper = mount(MessageActions, { props: { message: emptyMessage, }, }); const copyIcon = wrapper.find('[icon="files"]'); await copyIcon.trigger('click'); expect(vi.mocked(chatEventBus.emit)).not.toHaveBeenCalled(); }); });