import type { ComponentProps, ReactNode } from 'react' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentLogDetailResponse } from '@/models/log' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { useStore as useAppStore } from '@/app/components/app/store' import { fetchAgentLogDetail } from '@/service/log' import AgentLogDetail from '../detail' const { mockToast } = vi.hoisted(() => { const mockToast = Object.assign(vi.fn(), { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn(), dismiss: vi.fn(), update: vi.fn(), promise: vi.fn(), }) return { mockToast } }) vi.mock('@/service/log', () => ({ fetchAgentLogDetail: vi.fn(), })) vi.mock('@/app/components/base/ui/toast', () => ({ toast: mockToast, })) vi.mock('@/app/components/app/store', () => ({ useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })), })) vi.mock('@/app/components/workflow/run/status', () => ({ default: ({ status, time, tokens, error }: { status: string, time?: number, tokens?: number, error?: string }) => (
{error ? {String(error)} : null}
), })) vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({ default: ({ title, value }: { title: ReactNode, value: string | object }) => (
{title} {typeof value === 'string' ? value : JSON.stringify(value)}
), })) vi.mock('@/hooks/use-timestamp', () => ({ default: () => ({ formatTime: (ts: number, fmt: string) => `${ts}-${fmt}` }), })) vi.mock('@/app/components/workflow/block-icon', () => ({ default: () =>
, })) vi.mock('@/app/components/base/icons/src/vender/line/arrows', () => ({ ChevronRight: (props: { className?: string }) =>
, })) const createMockLog = (overrides: Partial = {}): IChatItem => ({ id: 'msg-id', content: 'output content', isAnswer: false, conversationId: 'conv-id', input: 'user input', ...overrides, }) const createMockResponse = (overrides: Partial = {}): AgentLogDetailResponse => ({ meta: { status: 'succeeded', executor: 'User', start_time: '2023-01-01', elapsed_time: 1.0, total_tokens: 100, agent_mode: 'function_call', iterations: 1, }, iterations: [ { created_at: '', files: [], thought: '', tokens: 0, tool_raw: { inputs: '', outputs: '' }, tool_calls: [{ tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } }], }, ], files: [], ...overrides, }) describe('AgentLogDetail', () => { const renderComponent = (props: Partial> = {}) => { const defaultProps: ComponentProps = { conversationID: 'conv-id', messageID: 'msg-id', log: createMockLog(), } return render() } const renderAndWaitForData = async (props: Partial> = {}) => { const result = renderComponent(props) await waitFor(() => { expect(screen.queryByRole('status')).not.toBeInTheDocument() }) return result } beforeEach(() => { vi.clearAllMocks() }) describe('Rendering', () => { it('should show loading indicator while fetching data', async () => { vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => { })) renderComponent() expect(screen.getByRole('status')).toBeInTheDocument() }) it('should display result panel after data loads', async () => { vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) await renderAndWaitForData() expect(screen.getByText(/runLog.detail/i)).toBeInTheDocument() expect(screen.getByText(/runLog.tracing/i)).toBeInTheDocument() }) it('should call fetchAgentLogDetail with correct params', async () => { vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) await renderAndWaitForData() expect(fetchAgentLogDetail).toHaveBeenCalledWith({ appID: 'app-id', params: { conversation_id: 'conv-id', message_id: 'msg-id', }, }) }) }) describe('Props', () => { it('should default to DETAIL tab when activeTab is not provided', async () => { vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) await renderAndWaitForData() const detailTab = screen.getByText(/runLog.detail/i) expect(detailTab.getAttribute('data-active')).toBe('true') }) it('should show TRACING tab when activeTab is TRACING', async () => { vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) await renderAndWaitForData({ activeTab: 'TRACING' }) const tracingTab = screen.getByText(/runLog.tracing/i) expect(tracingTab.getAttribute('data-active')).toBe('true') }) }) describe('User Interactions', () => { it('should switch to TRACING tab when clicked', async () => { vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) await renderAndWaitForData() fireEvent.click(screen.getByText(/runLog.tracing/i)) await waitFor(() => { const tracingTab = screen.getByText(/runLog.tracing/i) expect(tracingTab.getAttribute('data-active')).toBe('true') }) const detailTab = screen.getByText(/runLog.detail/i) expect(detailTab.getAttribute('data-active')).toBe('false') }) it('should switch back to DETAIL tab after switching to TRACING', async () => { vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) await renderAndWaitForData() fireEvent.click(screen.getByText(/runLog.tracing/i)) await waitFor(() => { expect(screen.getByText(/runLog.tracing/i).getAttribute('data-active')).toBe('true') }) fireEvent.click(screen.getByText(/runLog.detail/i)) await waitFor(() => { const detailTab = screen.getByText(/runLog.detail/i) expect(detailTab.getAttribute('data-active')).toBe('true') }) }) }) describe('Edge Cases', () => { it('should not fetch data when app detail is unavailable', async () => { vi.mocked(useAppStore).mockImplementationOnce(selector => selector({ appDetail: undefined } as never)) vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse()) renderComponent() await waitFor(() => { expect(fetchAgentLogDetail).not.toHaveBeenCalled() }) expect(screen.getByRole('status')).toBeInTheDocument() }) it('should notify on API error', async () => { vi.mocked(fetchAgentLogDetail).mockRejectedValue(new Error('API Error')) renderComponent() await waitFor(() => { expect(mockToast.error).toHaveBeenCalledWith('Error: API Error') }) }) it('should stop loading after API error', async () => { vi.mocked(fetchAgentLogDetail).mockRejectedValue(new Error('Network failure')) renderComponent() await waitFor(() => { expect(screen.queryByRole('status')).not.toBeInTheDocument() }) }) it('should handle response with empty iterations', async () => { vi.mocked(fetchAgentLogDetail).mockResolvedValue( createMockResponse({ iterations: [] }), ) await renderAndWaitForData() }) it('should handle response with multiple iterations and duplicate tools', async () => { const response = createMockResponse({ iterations: [ { created_at: '', files: [], thought: '', tokens: 0, tool_raw: { inputs: '', outputs: '' }, tool_calls: [ { tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } }, { tool_name: 'tool2', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 2' } }, ], }, { created_at: '', files: [], thought: '', tokens: 0, tool_raw: { inputs: '', outputs: '' }, tool_calls: [ { tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } }, ], }, ], }) vi.mocked(fetchAgentLogDetail).mockResolvedValue(response) await renderAndWaitForData() expect(screen.getByText(/runLog.detail/i)).toBeInTheDocument() }) }) })