/** * @license * Copyright 2025 AionUi (aionui.com) * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { IMessageAcpToolCall, IMessageToolCall } from '@/common/chat/chatLib'; import MessageToolGroupSummary from '@/renderer/pages/conversation/Messages/components/MessageToolGroupSummary'; const mockDownloadFileFromPath = vi.fn().mockResolvedValue(undefined); const mockMessageSuccess = vi.fn(); const mockMessageError = vi.fn(); vi.mock('@/renderer/components/media/LocalImageView', () => ({ __esModule: true, default: ({ src, alt, className }: { src: string; alt: string; className?: string }) => ( {alt} ), })); vi.mock('@/renderer/utils/file/download', () => ({ downloadFileFromPath: (...args: unknown[]) => mockDownloadFileFromPath(...args), })); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), })); vi.mock('@arco-design/web-react', async () => { const actual = await vi.importActual('@arco-design/web-react'); return { ...actual, Message: { useMessage: () => [{ success: mockMessageSuccess, error: mockMessageError }, null], }, }; }); describe('MessageToolGroupSummary ACP image output', () => { beforeEach(() => { mockDownloadFileFromPath.mockReset(); mockDownloadFileFromPath.mockResolvedValue(undefined); mockMessageSuccess.mockClear(); mockMessageError.mockClear(); }); it('renders generated image preview when an ACP image tool call is expanded', () => { const message: IMessageAcpToolCall = { id: 'ig_test_image', conversation_id: 'conv-1', type: 'acp_tool_call', content: { sessionId: 'sess-1', update: { sessionUpdate: 'tool_call_update', tool_call_id: 'ig_test_image', status: 'completed', title: 'Image generation', kind: 'execute', raw_output: { image: { path: '/Users/test/.codex/generated_images/session/ig_test_image.png', }, }, content: [ { type: 'content', content: { type: 'text', text: 'Revised prompt: 一张小猫照片', }, }, ], }, }, }; render(); fireEvent.click(screen.getByText('View Steps · 1')); const image = screen.getByTestId('local-image'); expect(image).toHaveAttribute('src', '/Users/test/.codex/generated_images/session/ig_test_image.png'); expect(image).toHaveAttribute('alt', 'ig_test_image.png'); }); it('downloads the generated image from its local path', () => { const imagePath = '/Users/test/.codex/generated_images/session/ig_test_image.png'; const message: IMessageAcpToolCall = { id: 'ig_test_image', conversation_id: 'conv-1', type: 'acp_tool_call', content: { sessionId: 'sess-1', update: { sessionUpdate: 'tool_call_update', tool_call_id: 'ig_test_image', status: 'completed', title: 'Image generation', kind: 'execute', raw_output: { image: { path: imagePath, }, }, }, }, }; render(); fireEvent.click(screen.getByText('View Steps · 1')); fireEvent.click(screen.getByLabelText('acp.image.download_aria')); expect(mockDownloadFileFromPath).toHaveBeenCalledWith(imagePath, 'ig_test_image.png'); }); it('shows an error when generated image download fails', async () => { const imagePath = '/Users/test/.codex/generated_images/session/ig_test_image.png'; const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); mockDownloadFileFromPath.mockRejectedValueOnce(new Error('denied')); const message: IMessageAcpToolCall = { id: 'ig_test_image', conversation_id: 'conv-1', type: 'acp_tool_call', content: { sessionId: 'sess-1', update: { sessionUpdate: 'tool_call_update', tool_call_id: 'ig_test_image', status: 'completed', title: 'Image generation', kind: 'execute', raw_output: { image: { path: imagePath, }, }, }, }, }; render(); fireEvent.click(screen.getByText('View Steps · 1')); fireEvent.click(screen.getByLabelText('acp.image.download_aria')); await waitFor(() => { expect(mockMessageError).toHaveBeenCalledWith('acp.image.download_error'); }); expect(consoleError).toHaveBeenCalledWith('[MessageToolGroupSummary] Failed to download image:', expect.any(Error)); expect(mockMessageSuccess).not.toHaveBeenCalled(); consoleError.mockRestore(); }); it('uses i18n keys for the image download control', () => { const message: IMessageAcpToolCall = { id: 'ig_test_image', conversation_id: 'conv-1', type: 'acp_tool_call', content: { sessionId: 'sess-1', update: { sessionUpdate: 'tool_call_update', tool_call_id: 'ig_test_image', status: 'completed', title: 'Image generation', kind: 'execute', raw_output: { image: { path: '/Users/test/.codex/generated_images/session/ig_test_image.png', }, }, }, }, }; render(); fireEvent.click(screen.getByText('View Steps · 1')); expect(screen.getByLabelText('acp.image.download_aria')).toBeInTheDocument(); }); it('does not render image controls for tool calls without image output', () => { const message: IMessageToolCall = { id: 'tool-1', conversation_id: 'conv-1', type: 'tool_call', content: { call_id: 'tool-1', name: 'Shell Command', args: {}, status: 'completed', }, }; render(); fireEvent.click(screen.getByText('View Steps · 1')); expect(screen.queryByTestId('local-image')).not.toBeInTheDocument(); expect(screen.queryByLabelText('acp.image.download_aria')).not.toBeInTheDocument(); }); });