Files
mixvideo-v2/apps/desktop/src/tests/components/EnhancedMarkdownRenderer.test.tsx
2025-07-22 15:04:37 +08:00

348 lines
10 KiB
TypeScript

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(
<EnhancedMarkdownRenderer
content="# Test\n\nHello **world**"
enableMarkdown={true}
/>
);
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(
<EnhancedMarkdownRenderer
content="# Test\n\nHello **world**"
enableMarkdown={false}
/>
);
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(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
/>
);
expect(screen.getByText('解析中...')).toBeInTheDocument();
});
it('shows error state when parsing fails', async () => {
mockMarkdownService.parseMarkdown.mockRejectedValue(new Error('Parse error'));
render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
/>
);
await waitFor(() => {
expect(screen.getByText('解析错误')).toBeInTheDocument();
expect(screen.getByText('Parse error')).toBeInTheDocument();
});
});
it('shows statistics when enabled', async () => {
render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
showStatistics={true}
/>
);
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(
<EnhancedMarkdownRenderer
content="# Test\n\n### Skipped"
enableMarkdown={true}
/>
);
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(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
enableReferences={true}
groundingMetadata={groundingMetadata}
/>
);
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(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
enableRealTimeParsing={true}
/>
);
// Change content
rerender(
<EnhancedMarkdownRenderer
content="# Updated Test"
enableMarkdown={true}
enableRealTimeParsing={true}
/>
);
// Wait for debounced parsing
await waitFor(() => {
expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledWith('# Updated Test');
}, { timeout: 1000 });
});
it('applies custom className', () => {
const { container } = render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
className="custom-class"
/>
);
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(
<EnhancedMarkdownRenderer
content="# H1\n## H2"
enableMarkdown={true}
/>
);
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');
});
});
});