fix: stabilize mermaid diagram reload clicks

This commit is contained in:
lucaronin
2026-05-17 00:24:17 +02:00
parent 7adecaea60
commit 28e2072f9b
3 changed files with 59 additions and 2 deletions

View File

@@ -59,6 +59,28 @@ describe('MermaidDiagram', () => {
expect(screen.getByTestId('mermaid-diagram-dialog-viewport').querySelector('svg')).not.toBeNull()
})
it('keeps rendered SVG pointer events inside the Mermaid block', async () => {
const onBlockPointer = vi.fn()
render(
<div onClick={onBlockPointer} onMouseDown={onBlockPointer}>
<MermaidDiagram
diagram={'flowchart LR\nA --> B'}
source={'```mermaid\nflowchart LR\nA --> B\n```'}
/>
</div>,
)
await waitFor(() => {
expect(screen.getByTestId('mermaid-diagram-viewport').querySelector('svg')).not.toBeNull()
})
const viewport = screen.getByTestId('mermaid-diagram-viewport')
fireEvent.mouseDown(viewport)
fireEvent.click(viewport)
expect(onBlockPointer).not.toHaveBeenCalled()
})
it('tags Mermaid SVG style elements with the runtime CSP nonce', async () => {
mermaidMock.render.mockResolvedValueOnce({
svg: '<svg aria-label="Rendered Mermaid"><style>.node{fill:#000}</style><g><text>A to B</text></g></svg>',

View File

@@ -1,5 +1,5 @@
import { ArrowsOut as Maximize2 } from '@phosphor-icons/react'
import { useEffect, useId, useMemo, useState } from 'react'
import { useEffect, useId, useMemo, useState, type SyntheticEvent } from 'react'
import { Button } from '@/components/ui/button'
import {
Dialog,
@@ -106,14 +106,27 @@ function MermaidSvgViewport({ ariaLabel, className, svg, testId }: MermaidSvgVie
<SafeSvgDiv
aria-label={ariaLabel}
className={className}
contentEditable={false}
data-testid={testId}
draggable={false}
onClick={stopMermaidViewportEvent}
onDoubleClick={stopMermaidViewportEvent}
onMouseDown={stopMermaidViewportEvent}
onMouseUp={stopMermaidViewportEvent}
onPointerDown={stopMermaidViewportEvent}
onPointerUp={stopMermaidViewportEvent}
role="img"
svg={svg}
suppressContentEditableWarning
tabIndex={0}
/>
)
}
function stopMermaidViewportEvent(event: SyntheticEvent): void {
event.stopPropagation()
}
function MermaidLightbox({ svg }: { svg: string }) {
return (
<Dialog>

View File

@@ -5,7 +5,7 @@ import { createFixtureVaultCopy, openFixtureVault, removeFixtureVaultCopy } from
import { executeCommand, openCommandPalette } from './helpers'
import { RUNTIME_STYLE_NONCE } from '../../src/lib/runtimeStyleNonce'
import { APP_COMMAND_IDS } from '../../src/hooks/appCommandCatalog'
import { triggerShortcutCommand } from './testBridge'
import { triggerMenuCommand, triggerShortcutCommand } from './testBridge'
let tempVaultDir: string
@@ -192,6 +192,13 @@ async function expectRenderedDiagramCount(page: Page, count: number): Promise<vo
await expect(page.locator('[data-testid="mermaid-diagram-viewport"] svg')).toHaveCount(count, { timeout: 15_000 })
}
async function reloadVault(page: Page): Promise<void> {
await triggerMenuCommand(page, APP_COMMAND_IDS.vaultReload)
await expect(page.getByText(/Vault reloaded \(\d+ entries\)/).last()).toBeVisible({
timeout: 5_000,
})
}
async function countLargeBlackFilledShapes(page: Page, diagramIndex: number): Promise<number> {
return page.locator('[data-testid="mermaid-diagram-viewport"] svg').nth(diagramIndex).evaluate((svg) => {
const shapes = Array.from(svg.querySelectorAll<SVGGraphicsElement>('rect,path,polygon'))
@@ -342,6 +349,21 @@ test('Mermaid diagrams stay mounted after property edits refresh frontmatter', a
expect(pageErrors).toEqual([])
})
test('Mermaid diagram clicks stay stable while a vault reload settles', async ({ page }) => {
const pageErrors: string[] = []
page.on('pageerror', error => pageErrors.push(error.message))
await openNote(page, 'Mermaid Reported')
await expectRenderedDiagramCount(page, 1)
await reloadVault(page)
await mermaidSvg(page, 0).click()
await expectRenderedDiagramCount(page, 1)
await expect(page.locator('#tolaria-fatal-render-error')).toHaveCount(0)
await expect(mermaidSvg(page, 0)).toContainText('Linked to a planned shift?')
expect(pageErrors).toEqual([])
})
test('Mermaid diagrams render, fall back, and round-trip through raw mode', async ({ page }) => {
await openNote(page, 'Note B')
await toggleRawMode(page, '.cm-content')