import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { EnhancedMarkdownRenderer } from '../../components/EnhancedMarkdownRenderer'; import { markdownService } from '../../services/markdownService'; import { MarkdownParseResult, MarkdownNode, MarkdownNodeType, ValidationResult, } from '../../types/markdown'; // Mock the markdown service vi.mock('../../services/markdownService', () => ({ markdownService: { parseMarkdown: vi.fn(), validateMarkdown: vi.fn(), extractTextContent: vi.fn(), }, })); const mockMarkdownService = markdownService as any; describe('EnhancedMarkdownRenderer', () => { const mockParseResult: MarkdownParseResult = { root: { node_type: MarkdownNodeType.Document, content: '# Test\n\nHello **world**', range: { start: { line: 0, column: 0, offset: 0 }, end: { line: 2, column: 12, offset: 19 }, }, children: [ { node_type: MarkdownNodeType.Heading, content: '# Test', range: { start: { line: 0, column: 0, offset: 0 }, end: { line: 0, column: 6, offset: 6 }, }, children: [ { node_type: MarkdownNodeType.Text, content: 'Test', range: { start: { line: 0, column: 2, offset: 2 }, end: { line: 0, column: 6, offset: 6 }, }, children: [], attributes: {}, }, ], attributes: { level: '1' }, }, { node_type: MarkdownNodeType.Paragraph, content: 'Hello **world**', range: { start: { line: 2, column: 0, offset: 8 }, end: { line: 2, column: 15, offset: 23 }, }, children: [ { node_type: MarkdownNodeType.Text, content: 'Hello ', range: { start: { line: 2, column: 0, offset: 8 }, end: { line: 2, column: 6, offset: 14 }, }, children: [], attributes: {}, }, { node_type: MarkdownNodeType.Strong, content: '**world**', range: { start: { line: 2, column: 6, offset: 14 }, end: { line: 2, column: 15, offset: 23 }, }, children: [ { node_type: MarkdownNodeType.Text, content: 'world', range: { start: { line: 2, column: 8, offset: 16 }, end: { line: 2, column: 13, offset: 21 }, }, children: [], attributes: {}, }, ], attributes: {}, }, ], attributes: {}, }, ], attributes: {}, }, statistics: { total_nodes: 5, error_nodes: 0, parse_time_ms: 3, document_length: 19, max_depth: 3, }, source_text: '# Test\n\nHello **world**', }; const mockValidation: ValidationResult = { is_valid: true, issues: [], statistics: { total_nodes: 5, error_nodes: 0, parse_time_ms: 3, document_length: 19, max_depth: 3, }, }; beforeEach(() => { mockMarkdownService.parseMarkdown.mockResolvedValue(mockParseResult); mockMarkdownService.validateMarkdown.mockResolvedValue(mockValidation); mockMarkdownService.extractTextContent.mockImplementation((node: MarkdownNode) => { if (node.node_type === MarkdownNodeType.Text) { return node.content; } return node.children.map(child => mockMarkdownService.extractTextContent(child)).join(''); }); }); afterEach(() => { vi.clearAllMocks(); }); it('renders markdown content using MarkdownService', async () => { render( ); await waitFor(() => { expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledWith(expect.stringContaining('# Test')); }); await waitFor(() => { expect(screen.getByText('Test')).toBeInTheDocument(); expect(screen.getByText('Hello')).toBeInTheDocument(); expect(screen.getByText('world')).toBeInTheDocument(); }); }); it('renders plain text when markdown is disabled', () => { render( ); expect(screen.getByText(/# Test.*Hello \*\*world\*\*/s)).toBeInTheDocument(); expect(mockMarkdownService.parseMarkdown).not.toHaveBeenCalled(); }); it('shows loading state during parsing', () => { // Make parseMarkdown return a pending promise mockMarkdownService.parseMarkdown.mockReturnValue(new Promise(() => {})); render( ); expect(screen.getByText('解析中...')).toBeInTheDocument(); }); it('shows error state when parsing fails', async () => { mockMarkdownService.parseMarkdown.mockRejectedValue(new Error('Parse error')); render( ); await waitFor(() => { expect(screen.getByText('解析错误')).toBeInTheDocument(); expect(screen.getByText('Parse error')).toBeInTheDocument(); }); }); it('shows statistics when enabled', async () => { render( ); await waitFor(() => { expect(screen.getByText('解析统计')).toBeInTheDocument(); expect(screen.getByText('节点数: 5')).toBeInTheDocument(); expect(screen.getByText('解析时间: 3ms')).toBeInTheDocument(); }); }); it('shows validation warnings when document is invalid', async () => { const invalidValidation: ValidationResult = { is_valid: false, issues: [ { issue_type: 'SkippedHeadingLevel' as any, message: 'Heading level jumps from 1 to 3', range: { start: { line: 2, column: 0, offset: 10 }, end: { line: 2, column: 10, offset: 20 }, }, severity: 'Warning' as any, }, ], statistics: mockValidation.statistics, }; mockMarkdownService.validateMarkdown.mockResolvedValue(invalidValidation); render( ); await waitFor(() => { expect(screen.getByText('文档验证警告')).toBeInTheDocument(); expect(screen.getByText('• Heading level jumps from 1 to 3')).toBeInTheDocument(); }); }); it('renders grounding metadata when provided', async () => { const groundingMetadata = { sources: [ { title: 'Source 1', url: 'https://example.com/1' }, { title: 'Source 2', url: 'https://example.com/2' }, ], }; render( ); await waitFor(() => { expect(screen.getByText('基于 2 个来源的信息')).toBeInTheDocument(); expect(screen.getByText('1')).toBeInTheDocument(); expect(screen.getByText('2')).toBeInTheDocument(); }); }); it('handles real-time parsing when enabled', async () => { const { rerender } = render( ); // Change content rerender( ); // Wait for debounced parsing await waitFor(() => { expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledWith('# Updated Test'); }, { timeout: 1000 }); }); it('applies custom className', () => { const { container } = render( ); expect(container.firstChild).toHaveClass('custom-class'); }); it('renders different heading levels correctly', async () => { const headingResult: MarkdownParseResult = { ...mockParseResult, root: { ...mockParseResult.root, children: [ { node_type: MarkdownNodeType.Heading, content: '# H1', range: { start: { line: 0, column: 0, offset: 0 }, end: { line: 0, column: 4, offset: 4 } }, children: [{ node_type: MarkdownNodeType.Text, content: 'H1', range: { start: { line: 0, column: 2, offset: 2 }, end: { line: 0, column: 4, offset: 4 } }, children: [], attributes: {} }], attributes: { level: '1' }, }, { node_type: MarkdownNodeType.Heading, content: '## H2', range: { start: { line: 1, column: 0, offset: 5 }, end: { line: 1, column: 5, offset: 10 } }, children: [{ node_type: MarkdownNodeType.Text, content: 'H2', range: { start: { line: 1, column: 3, offset: 8 }, end: { line: 1, column: 5, offset: 10 } }, children: [], attributes: {} }], attributes: { level: '2' }, }, ], }, }; mockMarkdownService.parseMarkdown.mockResolvedValue(headingResult); render( ); await waitFor(() => { const h1 = screen.getByRole('heading', { level: 1 }); const h2 = screen.getByRole('heading', { level: 2 }); expect(h1.textContent).toBe('H1'); expect(h2.textContent).toBe('H2'); }); }); });