feat(renderer): MermaidLazy (loads mermaid on first use, error UI)

This commit is contained in:
2026-06-05 13:01:24 +05:30
parent f34775ce53
commit d02f896167
2 changed files with 73 additions and 0 deletions
@@ -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<string | null>(null);
const [error, setError] = useState<string | null>(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 <div className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">{error}</div>;
if (!svg) return <div className="text-xs text-muted-foreground">Loading diagram</div>;
return <div data-testid="mermaid-output" dangerouslySetInnerHTML={{ __html: svg }} />;
}
@@ -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('<svg></svg>'),
},
}));
describe('MermaidLazy', () => {
it('renders an svg after mermaid resolves', async () => {
render(<MermaidLazy code="graph TD; A-->B" />);
await waitFor(() => {
expect(screen.getByTestId('mermaid-output').innerHTML).toContain('<svg');
});
});
it('shows an error message if mermaid throws', async () => {
const mermaid = (await import('mermaid')).default as any;
mermaid.render.mockRejectedValueOnce(new Error('mermaid failed'));
render(<MermaidLazy code="bad code" />);
await waitFor(() => {
expect(screen.getByText(/mermaid failed/i)).toBeInTheDocument();
});
});
});