import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import mermaid from 'mermaid' import Flowchart from './index' vi.mock('mermaid', () => ({ default: { initialize: vi.fn(), render: vi.fn().mockResolvedValue({ svg: 'test-svg', diagramType: 'flowchart' }), mermaidAPI: { render: vi.fn().mockResolvedValue({ svg: 'test-svg-api', diagramType: 'flowchart' }), }, }, })) vi.mock('./utils', async (importOriginal) => { const actual = await importOriginal() as Record return { ...actual, svgToBase64: vi.fn().mockResolvedValue('data:image/svg+xml;base64,dGVzdC1zdmc='), waitForDOMElement: vi.fn((cb: () => Promise) => cb()), } }) describe('Mermaid Flowchart Component', () => { const mockCode = 'graph TD\n A-->B' beforeEach(() => { vi.clearAllMocks() vi.mocked(mermaid.initialize).mockImplementation(() => { }) }) describe('Rendering', () => { it('should initialize mermaid on mount', async () => { await act(async () => { render() }) expect(mermaid.initialize).toHaveBeenCalled() }) it('should render mermaid chart after debounce', async () => { await act(async () => { render() }) await waitFor(() => { expect(screen.getByText('test-svg')).toBeInTheDocument() }, { timeout: 3000 }) }) it('should render gantt charts with specific formatting', async () => { const ganttCode = 'gantt\ntitle T\nTask :after task1, after task2' await act(async () => { render() }) await waitFor(() => { expect(screen.getByText('test-svg')).toBeInTheDocument() }, { timeout: 3000 }) }) it('should render mindmap and sequenceDiagram charts', async () => { const mindmapCode = 'mindmap\n root\n topic1' const { unmount } = await act(async () => { return render() }) await waitFor(() => { expect(screen.getByText('test-svg')).toBeInTheDocument() }, { timeout: 3000 }) unmount() const sequenceCode = 'sequenceDiagram\n A->>B: Hello' await act(async () => { render() }) await waitFor(() => { expect(screen.getByText('test-svg')).toBeInTheDocument() }, { timeout: 3000 }) }) it('should handle dark theme configuration', async () => { await act(async () => { render() }) await waitFor(() => { expect(screen.getByText('test-svg')).toBeInTheDocument() }, { timeout: 3000 }) }) }) describe('Interactions', () => { it('should switch between classic and handDrawn looks', async () => { await act(async () => { render() }) await waitFor(() => screen.getByText('test-svg'), { timeout: 3000 }) const handDrawnBtn = screen.getByText(/handDrawn/i) await act(async () => { fireEvent.click(handDrawnBtn) }) await waitFor(() => { expect(screen.getByText('test-svg-api')).toBeInTheDocument() }, { timeout: 3000 }) const classicBtn = screen.getByText(/classic/i) await act(async () => { fireEvent.click(classicBtn) }) await waitFor(() => { expect(screen.getByText('test-svg')).toBeInTheDocument() }, { timeout: 3000 }) }) it('should toggle theme manually', async () => { await act(async () => { render() }) await waitFor(() => screen.getByText('test-svg'), { timeout: 3000 }) const toggleBtn = screen.getByRole('button') await act(async () => { fireEvent.click(toggleBtn) }) await waitFor(() => { expect(mermaid.initialize).toHaveBeenCalled() }, { timeout: 3000 }) }) it('should open image preview when clicking the chart', async () => { await act(async () => { render() }) await waitFor(() => screen.getByText('test-svg'), { timeout: 3000 }) const chartDiv = screen.getByText('test-svg').closest('.mermaid') await act(async () => { fireEvent.click(chartDiv!) }) await waitFor(() => { expect(document.body.querySelector('.image-preview-container')).toBeInTheDocument() }, { timeout: 3000 }) }) }) describe('Edge Cases', () => { it('should not render when code is too short', async () => { const shortCode = 'graph' vi.useFakeTimers() render() await vi.advanceTimersByTimeAsync(1000) expect(mermaid.render).not.toHaveBeenCalled() vi.useRealTimers() }) it('should handle rendering errors gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }) const errorMsg = 'Syntax error' vi.mocked(mermaid.render).mockRejectedValue(new Error(errorMsg)) // Use unique code to avoid hitting the module-level diagramCache from previous tests const uniqueCode = 'graph TD\n X-->Y\n Y-->Z' const { container } = render() await waitFor(() => { const errorSpan = container.querySelector('.text-red-500 span.ml-2') expect(errorSpan).toBeInTheDocument() expect(errorSpan?.textContent).toContain('Rendering failed') }, { timeout: 5000 }) consoleSpy.mockRestore() // Restore default mock to prevent leaking into subsequent tests vi.mocked(mermaid.render).mockResolvedValue({ svg: 'test-svg', diagramType: 'flowchart' }) }, 10000) it('should use cached diagram if available', async () => { const { rerender } = render() await waitFor(() => screen.getByText('test-svg'), { timeout: 3000 }) vi.mocked(mermaid.render).mockClear() await act(async () => { rerender() }) await act(async () => { await new Promise(resolve => setTimeout(resolve, 500)) }) expect(mermaid.render).not.toHaveBeenCalled() }) it('should handle invalid mermaid code completion', async () => { const invalidCode = 'graph TD\nA -->' // Incomplete await act(async () => { render() }) await waitFor(() => { expect(screen.getByText('Diagram code is not complete or invalid.')).toBeInTheDocument() }, { timeout: 3000 }) }) it('should handle unmount cleanup', async () => { const { unmount } = render() await act(async () => { unmount() }) }) }) }) describe('Mermaid Flowchart Component Module Isolation', () => { const mockCode = 'graph TD\n A-->B' let mermaidFresh: typeof mermaid beforeEach(async () => { vi.resetModules() vi.clearAllMocks() const mod = await import('mermaid') as unknown as { default: typeof mermaid } | typeof mermaid mermaidFresh = 'default' in mod ? mod.default : mod vi.mocked(mermaidFresh.initialize).mockImplementation(() => { }) }) describe('Error Handling', () => { it('should handle initialization failure', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }) const { default: FlowchartFresh } = await import('./index') vi.mocked(mermaidFresh.initialize).mockImplementationOnce(() => { throw new Error('Init fail') }) await act(async () => { render() }) expect(consoleSpy).toHaveBeenCalledWith('Mermaid initialization error:', expect.any(Error)) consoleSpy.mockRestore() }) it('should handle mermaidAPI missing fallback', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }) const originalMermaidAPI = mermaidFresh.mermaidAPI // @ts-expect-error need to set undefined for testing mermaidFresh.mermaidAPI = undefined const { default: FlowchartFresh } = await import('./index') const { container } = render() // Wait for initial render to complete await waitFor(() => { expect(screen.getByText(/handDrawn/)).toBeInTheDocument() }, { timeout: 3000 }) const handDrawnBtn = screen.getByText(/handDrawn/) await act(async () => { fireEvent.click(handDrawnBtn) }) // When mermaidAPI is undefined, handDrawn style falls back to mermaid.render. // The module captures mermaidAPI at import time, so setting it to undefined on // the mocked object may not affect the module's internal reference. // Verify that the rendering completes (either with svg or error) await waitFor(() => { const hasSvg = container.querySelector('.mermaid div') const hasError = container.querySelector('.text-red-500') expect(hasSvg || hasError).toBeTruthy() }, { timeout: 5000 }) mermaidFresh.mermaidAPI = originalMermaidAPI consoleSpy.mockRestore() }, 10000) it('should handle configuration failure', async () => { vi.mocked(mermaidFresh.initialize).mockImplementation(() => { throw new Error('Config fail') }) const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }) const { default: FlowchartFresh } = await import('./index') await act(async () => { render() }) await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith('Mermaid initialization error:', expect.any(Error)) }) consoleSpy.mockRestore() }) }) })