From 28e2072f9bc06cbf24ede64ab58c88b24d9664c2 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 17 May 2026 00:24:17 +0200 Subject: [PATCH] fix: stabilize mermaid diagram reload clicks --- src/components/MermaidDiagram.test.tsx | 22 +++++++++++++++++++++ src/components/MermaidDiagram.tsx | 15 +++++++++++++- tests/smoke/mermaid-diagram-notes.spec.ts | 24 ++++++++++++++++++++++- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/components/MermaidDiagram.test.tsx b/src/components/MermaidDiagram.test.tsx index 43721925..54e2cac2 100644 --- a/src/components/MermaidDiagram.test.tsx +++ b/src/components/MermaidDiagram.test.tsx @@ -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( +
+ B'} + source={'```mermaid\nflowchart LR\nA --> B\n```'} + /> +
, + ) + + 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: 'A to B', diff --git a/src/components/MermaidDiagram.tsx b/src/components/MermaidDiagram.tsx index 26380d1a..ff0d5ada 100644 --- a/src/components/MermaidDiagram.tsx +++ b/src/components/MermaidDiagram.tsx @@ -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 ) } +function stopMermaidViewportEvent(event: SyntheticEvent): void { + event.stopPropagation() +} + function MermaidLightbox({ svg }: { svg: string }) { return ( diff --git a/tests/smoke/mermaid-diagram-notes.spec.ts b/tests/smoke/mermaid-diagram-notes.spec.ts index 8af090fa..4f52e0cd 100644 --- a/tests/smoke/mermaid-diagram-notes.spec.ts +++ b/tests/smoke/mermaid-diagram-notes.spec.ts @@ -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 { + 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 { return page.locator('[data-testid="mermaid-diagram-viewport"] svg').nth(diagramIndex).evaluate((svg) => { const shapes = Array.from(svg.querySelectorAll('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')