import React from 'react';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { setLocale } from '../src/i18n';
import ChatMessageList from '../src/components/chat/ChatMessageList';
const mocks = vi.hoisted(() => ({
apiOpenSkillPath: vi.fn(),
apiOpenSkillReadme: vi.fn(),
writeText: vi.fn(),
}));
vi.mock('../src/lib/skills-api', () => ({
apiOpenSkillPath: mocks.apiOpenSkillPath,
apiOpenSkillReadme: mocks.apiOpenSkillReadme,
}));
describe('ChatMessageList', () => {
beforeEach(() => {
setLocale('en');
vi.clearAllMocks();
mocks.apiOpenSkillPath.mockResolvedValue(undefined);
mocks.apiOpenSkillReadme.mockResolvedValue(undefined);
mocks.writeText.mockResolvedValue(undefined);
Object.defineProperty(window.navigator, 'clipboard', {
value: {
writeText: mocks.writeText,
},
configurable: true,
});
});
it('renders standalone streaming tool status cards when there is no streaming assistant text yet', () => {
render(
,
);
expect(screen.getByText('Running tools...')).toBeTruthy();
expect(screen.getByText('Install Skill')).toBeTruthy();
expect(screen.getByText('Running')).toBeTruthy();
expect(screen.getByText('Installing minimax-xlsx')).toBeTruthy();
});
it('renders streaming tool status cards above a streaming assistant message', () => {
render(
,
);
expect(screen.getByText('Open Webpage')).toBeTruthy();
expect(screen.getByText('Completed')).toBeTruthy();
expect(screen.getByText('1.2s')).toBeTruthy();
expect(screen.getByText('Opened http://www.baidu.com/')).toBeTruthy();
expect(screen.getByText('The page is open.')).toBeTruthy();
});
it('renders persistent skill-install tool cards with follow-up actions', async () => {
render(
,
);
expect(screen.getAllByText('Installed and enabled minimax-xlsx at /tmp/minimax-xlsx')).toHaveLength(1);
expect(screen.getByText('Skill')).toBeTruthy();
expect(screen.getByText('/tmp/minimax-xlsx')).toBeTruthy();
expect(screen.getByText('Next actions')).toBeTruthy();
fireEvent.click(screen.getByText('Open folder'));
await waitFor(() => {
expect(mocks.apiOpenSkillPath).toHaveBeenCalledWith(
'minimax-xlsx',
'minimax-xlsx',
'/tmp/minimax-xlsx',
);
});
fireEvent.click(screen.getByText('Copy path'));
await waitFor(() => {
expect(mocks.writeText).toHaveBeenCalledWith('/tmp/minimax-xlsx');
});
expect(screen.getByText('Path copied')).toBeTruthy();
});
it('renders spreadsheet analysis cards from structured tool results', () => {
render(
,
);
const spreadsheetCard = screen.getByTestId('tool-spreadsheet-preview');
expect(within(spreadsheetCard).getByText('Analysis overview')).toBeTruthy();
expect(within(spreadsheetCard).getByText('1 file')).toBeTruthy();
expect(within(spreadsheetCard).getAllByText('1 sheet').length).toBeGreaterThan(0);
expect(within(spreadsheetCard).getAllByText('18,358 rows').length).toBeGreaterThan(0);
expect(within(spreadsheetCard).getByText('hotel-sales.xls')).toBeTruthy();
expect(within(spreadsheetCard).getByText('Grid Results')).toBeTruthy();
expect(within(spreadsheetCard).getByText('12 cols')).toBeTruthy();
expect(within(spreadsheetCard).getByText(/Columns: channel, sales_amount, sales_date, room_type/)).toBeTruthy();
expect(within(spreadsheetCard).getByText('Douyin')).toBeTruthy();
expect(within(spreadsheetCard).getByText('615.53')).toBeTruthy();
expect(within(spreadsheetCard).getByText('sales_date missing in 2,566 rows')).toBeTruthy();
expect(screen.getByText('Artifacts')).toBeTruthy();
expect(screen.getByText('F:\\Downloads\\hotel-sales.xls')).toBeTruthy();
});
it('renders generic document-analysis structured data when there is no spreadsheet preview', () => {
render(
,
);
const structuredCard = screen.getByTestId('tool-structured-result');
expect(within(structuredCard).getByText('Structured result')).toBeTruthy();
expect(within(structuredCard).getByText('Confidence')).toBeTruthy();
expect(within(structuredCard).getByText('0.94')).toBeTruthy();
expect(within(structuredCard).getByText('Documents')).toBeTruthy();
expect(within(structuredCard).getByText('master-service-agreement.pdf')).toBeTruthy();
expect(within(structuredCard).getByText('Termination')).toBeTruthy();
expect(within(structuredCard).getByText('Needs review')).toBeTruthy();
});
it('renders dedicated search result cards for search-family tool results', () => {
render(
,
);
const searchCard = screen.getByTestId('tool-search-results');
expect(within(searchCard).getByText('Search overview')).toBeTruthy();
expect(within(searchCard).getByText('Provider: brave')).toBeTruthy();
expect(within(searchCard).getByText('2 results')).toBeTruthy();
expect(within(searchCard).getByText('Query')).toBeTruthy();
expect(within(searchCard).getByText('hotel revenue trends')).toBeTruthy();
expect(within(searchCard).getByText('Answer')).toBeTruthy();
expect(within(searchCard).getByText('Revenue continues to improve across the sector.')).toBeTruthy();
expect(within(searchCard).getByText('Hotel Revenue Trends Q1')).toBeTruthy();
expect(within(searchCard).getByText('https://example.com/revenue-q1')).toBeTruthy();
expect(within(searchCard).getByText('Source: Example News')).toBeTruthy();
expect(within(searchCard).getByText('Score: 0.9821')).toBeTruthy();
});
it('renders install commands in the search result card for command-style tool results', () => {
render(
,
);
const searchCard = screen.getByTestId('tool-search-results');
expect(within(searchCard).getByText('react-best-practices')).toBeTruthy();
expect(within(searchCard).getByText('Install command')).toBeTruthy();
expect(within(searchCard).getByText('npx skills add vercel-labs/agent-skills@react-best-practices -g -y')).toBeTruthy();
});
});