import { describe, it, expect, vi } from 'vitest'; import { render } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/dom'; import MarkdownContent from './MarkdownContent'; // Mock the icons to avoid import issues vi.mock('./icons', () => ({ Check: () =>
, Copy: () =>
📋
, })); describe('MarkdownContent', () => { describe('HTML Security Integration', () => { it('renders safe markdown content normally', async () => { const content = `# Test Title Visit for more info. Contact for support. Use \`Array\` for generics.`; render(); await waitFor(() => { expect(screen.getByText('Test Title')).toBeInTheDocument(); expect(screen.getByText(/Visit/)).toBeInTheDocument(); expect(screen.getByText(/for more info/)).toBeInTheDocument(); expect(screen.getByText(/Contact/)).toBeInTheDocument(); expect(screen.getByText(/for support/)).toBeInTheDocument(); }); // Should not create extra code blocks for safe content const codeBlocks = screen.queryAllByText(/```html/); expect(codeBlocks).toHaveLength(0); }); it('wraps dangerous HTML in code blocks', async () => { const content = `# Security Test This is safe text. More safe text.`; render(); await waitFor(() => { expect(screen.getByText('Security Test')).toBeInTheDocument(); expect(screen.getByText('This is safe text.')).toBeInTheDocument(); expect(screen.getByText('More safe text.')).toBeInTheDocument(); }); // The script tag should be in a code block, not executed const scriptElements = document.querySelectorAll('script'); expect(scriptElements).toHaveLength(0); // No actual script tags should be created // Should find the script content in a code block (text may be split across spans) await waitFor(() => { expect(screen.getByText(/alert/)).toBeInTheDocument(); expect(screen.getByText(/xss/)).toBeInTheDocument(); }); }); it('handles HTML comments securely', async () => { const content = `# Comment Test Normal text continues.`; render(); await waitFor(() => { expect(screen.getByText('Comment Test')).toBeInTheDocument(); expect(screen.getByText('Normal text continues.')).toBeInTheDocument(); }); // Comment should be in a code block await waitFor(() => { expect(screen.getByText(/This is a malicious comment/)).toBeInTheDocument(); }); }); it('preserves existing code blocks', async () => { const content = `# Code Block Test \`\`\`javascript const html = "
This is safe in a code block
"; console.log(html); \`\`\`
This should be wrapped
`; render(); await waitFor(() => { expect(screen.getByText('Code Block Test')).toBeInTheDocument(); }); // Should preserve the original JavaScript code block (text may be split) await waitFor(() => { expect(screen.getByText(/const/)).toBeInTheDocument(); expect(screen.getAllByText(/html/)).toHaveLength(2); // Variable name and function parameter }); // The div outside the code block should be wrapped await waitFor(() => { expect(screen.getByText(/This should be wrapped/)).toBeInTheDocument(); }); }); it('handles mixed safe and unsafe content', async () => { const content = `# Mixed Content Test 1. Auto-link: 2. Inline code: \`const x = Array();\` 3. Real markup: 4. Placeholder path: /src`; render(); await waitFor(() => { expect(screen.getByText('Mixed Content Test')).toBeInTheDocument(); expect(screen.getByText(/Auto-link/)).toBeInTheDocument(); expect(screen.getByText(/Inline code/)).toBeInTheDocument(); expect(screen.getByText(/Real markup/)).toBeInTheDocument(); expect(screen.getByText(/Placeholder path/)).toBeInTheDocument(); }); // Only the input tag should be wrapped await waitFor(() => { expect(screen.getByText(/input/)).toBeInTheDocument(); expect(screen.getByText(/type/)).toBeInTheDocument(); expect(screen.getByText(/disabled/)).toBeInTheDocument(); }); // Should not have actual input elements in the DOM const inputElements = document.querySelectorAll('input'); expect(inputElements).toHaveLength(0); }); }); describe('Code Block Functionality', () => { it('renders code blocks with syntax highlighting', async () => { const content = `\`\`\`javascript console.log('Hello, World!'); \`\`\``; render(); await waitFor(() => { expect(screen.getByText(/console/)).toBeInTheDocument(); expect(screen.getByText(/log/)).toBeInTheDocument(); expect(screen.getByText(/Hello, World!/)).toBeInTheDocument(); }); }); it('renders inline code', async () => { const content = 'Use `console.log()` to debug.'; render(); await waitFor(() => { expect(screen.getByText(/Use/)).toBeInTheDocument(); expect(screen.getByText(/to debug/)).toBeInTheDocument(); expect(screen.getByText('console.log()')).toBeInTheDocument(); }); }); }); describe('Markdown Features', () => { it('renders headers correctly', async () => { const content = `# H1 Header ## H2 Header ### H3 Header`; render(); await waitFor(() => { expect(screen.getByRole('heading', { level: 1, name: 'H1 Header' })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 2, name: 'H2 Header' })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 3, name: 'H3 Header' })).toBeInTheDocument(); }); }); it('renders lists correctly', async () => { const content = `- Item 1 - Item 2 - Item 3 1. Numbered 1 2. Numbered 2`; render(); await waitFor(() => { expect(screen.getByText('Item 1')).toBeInTheDocument(); expect(screen.getByText('Item 2')).toBeInTheDocument(); expect(screen.getByText('Item 3')).toBeInTheDocument(); expect(screen.getByText('Numbered 1')).toBeInTheDocument(); expect(screen.getByText('Numbered 2')).toBeInTheDocument(); }); }); it('renders links with correct attributes', async () => { const content = '[Visit Block](https://block.dev)'; render(); await waitFor(() => { const link = screen.getByRole('link', { name: 'Visit Block' }); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute('href', 'https://block.dev'); expect(link).toHaveAttribute('target', '_blank'); expect(link).toHaveAttribute('rel', 'noopener noreferrer'); }); }); it('renders tables correctly', async () => { const content = `| Name | Value | |------|-------| | Test | 123 | | Demo | 456 |`; render(); await waitFor(() => { expect(screen.getByText('Name')).toBeInTheDocument(); expect(screen.getByText('Value')).toBeInTheDocument(); expect(screen.getByText('Test')).toBeInTheDocument(); expect(screen.getByText('123')).toBeInTheDocument(); expect(screen.getByText('Demo')).toBeInTheDocument(); expect(screen.getByText('456')).toBeInTheDocument(); }); }); }); describe('Error Handling', () => { it('handles empty content gracefully', async () => { render(); // Should not throw and should render the component const container = document.querySelector('.w-full.overflow-x-hidden'); expect(container).toBeInTheDocument(); }); it('handles malformed markdown gracefully', async () => { const content = `# Unclosed header [Unclosed link(https://example.com \`\`\` Unclosed code block`; render(); await waitFor(() => { // Should still render what it can expect(screen.getByText('Unclosed header')).toBeInTheDocument(); }); }); }); describe('Line Break Functionality', () => { it('preserves single line breaks with remark-breaks plugin', async () => { const content = `First line Second line Third line`; const { container } = render(); await waitFor(() => { // Check that all text content is present (text may be split by
tags) expect(container).toHaveTextContent('First line'); expect(container).toHaveTextContent('Second line'); expect(container).toHaveTextContent('Third line'); }); // Check that line breaks are preserved (rendered as
tags) const brElements = container.querySelectorAll('br'); expect(brElements.length).toBeGreaterThan(0); }); it('handles mixed content with line breaks', async () => { const content = `# Header Paragraph with line breaks. - List item 1 - List item 2`; const { container } = render(); await waitFor(() => { expect(screen.getByRole('heading', { level: 1, name: 'Header' })).toBeInTheDocument(); // Check that text content is present (text may be split by
tags) expect(container).toHaveTextContent('Paragraph with'); expect(container).toHaveTextContent('line breaks.'); expect(screen.getByText('List item 1')).toBeInTheDocument(); expect(screen.getByText('List item 2')).toBeInTheDocument(); }); }); it('maintains existing markdown features with line breaks', async () => { const content = `**Bold text** with line break \`code\` and more text`; const { container } = render(); await waitFor(() => { // Bold text should still work const boldElement = container.querySelector('strong'); expect(boldElement).toBeInTheDocument(); expect(boldElement).toHaveTextContent('Bold text'); // Code should still work expect(screen.getByText('code')).toBeInTheDocument(); }); }); }); describe('URL Overflow Handling', () => { it('handles very long URLs without overflow', async () => { const longUrl = 'https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1'; const content = `Check out this document: ${longUrl} Another very long URL: https://www.example.com/very/long/path/with/many/segments/and/parameters?param1=value1¶m2=value2¶m3=value3¶m4=value4¶m5=value5`; const { container } = render(); await waitFor(() => { expect(screen.getByText(/Check out this document/)).toBeInTheDocument(); expect(screen.getByText(/Another very long URL/)).toBeInTheDocument(); }); // Check that URLs are rendered as links const links = container.querySelectorAll('a'); expect(links.length).toBeGreaterThan(0); // Check that links have proper CSS classes for word breaking links.forEach((link) => { // The CSS should allow the text to break expect(link).toBeInTheDocument(); }); }); it('handles markdown links with long URLs', async () => { const longUrl = 'https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1'; const content = `[Click here for the document](${longUrl})`; render(); await waitFor(() => { const link = screen.getByRole('link', { name: 'Click here for the document' }); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute('href', longUrl); }); }); it('handles multiple long URLs in the same message', async () => { const content = `Here are some long URLs: 1. Example Doc: https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1 2. Another long URL: https://www.example.com/very/long/path/with/many/segments/and/parameters?param1=value1¶m2=value2¶m3=value3 3. Third URL: https://api.example.com/v1/users/12345/documents/67890/attachments/abcdef123456789?format=json&include=metadata&sort=created_at`; render(); await waitFor(() => { expect(screen.getByText(/Here are some long URLs/)).toBeInTheDocument(); expect(screen.getByText(/Example Doc/)).toBeInTheDocument(); expect(screen.getByText(/Another long URL/)).toBeInTheDocument(); expect(screen.getByText(/Third URL/)).toBeInTheDocument(); }); }); it('applies word-break CSS classes to the container', () => { const content = 'Test content'; render(); const markdownContainer = document.querySelector('.prose'); expect(markdownContainer).toBeInTheDocument(); expect(markdownContainer).toHaveClass('prose-a:break-all'); expect(markdownContainer).toHaveClass('prose-a:overflow-wrap-anywhere'); }); }); });