From d02f8961676a18ffb3ba3a03874683e6b4f0666d Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 13:01:24 +0530 Subject: [PATCH] feat(renderer): MermaidLazy (loads mermaid on first use, error UI) --- .../components/preview/MermaidLazy.tsx | 45 +++++++++++++++++++ tests/component/preview/MermaidLazy.test.tsx | 28 ++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/renderer/components/preview/MermaidLazy.tsx create mode 100644 tests/component/preview/MermaidLazy.test.tsx diff --git a/src/renderer/components/preview/MermaidLazy.tsx b/src/renderer/components/preview/MermaidLazy.tsx new file mode 100644 index 0000000..6880b3d --- /dev/null +++ b/src/renderer/components/preview/MermaidLazy.tsx @@ -0,0 +1,45 @@ +import { useEffect, useRef, useState } from 'react'; + +interface Props { + code: string; +} + +let mermaidModule: typeof import('mermaid').default | null = null; +let initialized = false; + +async function getMermaid() { + if (!mermaidModule) { + const mod = await import('mermaid'); + mermaidModule = mod.default; + } + if (!initialized) { + mermaidModule.initialize({ startOnLoad: false, theme: 'default' }); + initialized = true; + } + return mermaidModule; +} + +export function MermaidLazy({ code }: Props) { + const [svg, setSvg] = useState(null); + const [error, setError] = useState(null); + const idRef = useRef(`mermaid-${Math.random().toString(36).slice(2)}`); + + useEffect(() => { + let cancelled = false; + getMermaid() + .then((m) => m.render(idRef.current, code)) + .then((rendered) => { + if (!cancelled) setSvg(rendered); + }) + .catch((err: Error) => { + if (!cancelled) setError(err.message); + }); + return () => { + cancelled = true; + }; + }, [code]); + + if (error) return
{error}
; + if (!svg) return
Loading diagram…
; + return
; +} diff --git a/tests/component/preview/MermaidLazy.test.tsx b/tests/component/preview/MermaidLazy.test.tsx new file mode 100644 index 0000000..332c6de --- /dev/null +++ b/tests/component/preview/MermaidLazy.test.tsx @@ -0,0 +1,28 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import { MermaidLazy } from '@/components/preview/MermaidLazy'; + +vi.mock('mermaid', () => ({ + default: { + initialize: vi.fn(), + render: vi.fn().mockResolvedValue(''), + }, +})); + +describe('MermaidLazy', () => { + it('renders an svg after mermaid resolves', async () => { + render(); + await waitFor(() => { + expect(screen.getByTestId('mermaid-output').innerHTML).toContain(' { + const mermaid = (await import('mermaid')).default as any; + mermaid.render.mockRejectedValueOnce(new Error('mermaid failed')); + render(); + await waitFor(() => { + expect(screen.getByText(/mermaid failed/i)).toBeInTheDocument(); + }); + }); +});