fix: preserve mermaid svg styles under tauri csp
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RUNTIME_STYLE_NONCE } from '../lib/runtimeStyleNonce'
|
||||
import { MermaidDiagram } from './MermaidDiagram'
|
||||
|
||||
const mermaidMock = vi.hoisted(() => ({
|
||||
@@ -50,6 +51,26 @@ describe('MermaidDiagram', () => {
|
||||
expect(screen.getByTestId('mermaid-diagram-dialog-viewport').querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
|
||||
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>',
|
||||
})
|
||||
|
||||
render(
|
||||
<MermaidDiagram
|
||||
diagram={'flowchart LR\nA --> B'}
|
||||
source={'```mermaid\nflowchart LR\nA --> B\n```'}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('mermaid-diagram-viewport').querySelector('style')).not.toBeNull()
|
||||
})
|
||||
|
||||
const style = screen.getByTestId('mermaid-diagram-viewport').querySelector('style')
|
||||
expect(style?.getAttribute('nonce')).toBe(RUNTIME_STYLE_NONCE)
|
||||
})
|
||||
|
||||
it('falls back to the original source when Mermaid cannot render', async () => {
|
||||
mermaidMock.render.mockRejectedValueOnce(new Error('parse error'))
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { RUNTIME_STYLE_NONCE } from '@/lib/runtimeStyleNonce'
|
||||
|
||||
type MermaidApi = typeof import('mermaid')['default']
|
||||
|
||||
@@ -51,6 +52,18 @@ function initializeMermaid(mermaid: MermaidApi) {
|
||||
initialized = true
|
||||
}
|
||||
|
||||
function withRuntimeStyleNonce(svg: string): string {
|
||||
if (!svg.includes('<style')) return svg
|
||||
if (typeof document === 'undefined') return svg
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = svg
|
||||
container.querySelectorAll('style').forEach((style) => {
|
||||
style.setAttribute('nonce', RUNTIME_STYLE_NONCE)
|
||||
})
|
||||
return container.innerHTML
|
||||
}
|
||||
|
||||
async function renderMermaidDiagram({
|
||||
diagram,
|
||||
renderId,
|
||||
@@ -62,7 +75,7 @@ async function renderMermaidDiagram({
|
||||
const mermaid = (await import('mermaid')).default
|
||||
initializeMermaid(mermaid)
|
||||
const result = await mermaid.render(renderId, diagram)
|
||||
return result.svg
|
||||
return withRuntimeStyleNonce(result.svg)
|
||||
}
|
||||
const nextRender = renderQueue.then(render, render)
|
||||
renderQueue = nextRender.then(() => undefined, () => undefined)
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { createFixtureVaultCopy, openFixtureVault, removeFixtureVaultCopy } from '../helpers/fixtureVault'
|
||||
import { executeCommand, openCommandPalette } from './helpers'
|
||||
import { RUNTIME_STYLE_NONCE } from '../../src/lib/runtimeStyleNonce'
|
||||
|
||||
let tempVaultDir: string
|
||||
|
||||
@@ -200,6 +201,13 @@ async function computedCss(locator: Locator, property: 'color' | 'fill' | 'strok
|
||||
), property)
|
||||
}
|
||||
|
||||
async function readStyleNonce(svg: Locator): Promise<string | null> {
|
||||
return svg.locator('style').first().evaluate((style) => {
|
||||
const nonce = (style as HTMLElement).nonce
|
||||
return nonce.length > 0 ? nonce : style.getAttribute('nonce')
|
||||
})
|
||||
}
|
||||
|
||||
async function labelFitsNode(node: Locator): Promise<boolean> {
|
||||
return node.evaluate((element) => {
|
||||
const shape = element.querySelector<SVGGraphicsElement>('rect, polygon, circle, ellipse, path')!
|
||||
@@ -230,6 +238,7 @@ async function readReportedDiagramMetrics(page: Page, diagramIndex: number) {
|
||||
await labelFitsNode(mermaidNode(page, diagramIndex, 'Contract path')),
|
||||
await labelFitsNode(mermaidNode(page, diagramIndex, 'unscheduled clocking')),
|
||||
].every(Boolean),
|
||||
styleNonce: await readStyleNonce(svg),
|
||||
text: await svg.evaluate((element) => element.textContent ?? ''),
|
||||
}
|
||||
}
|
||||
@@ -248,6 +257,7 @@ test('Mermaid diagrams render when opening saved notes directly', async ({ page
|
||||
scheduledStroke: 'rgb(122, 91, 0)',
|
||||
scheduledTextColor: 'rgb(58, 44, 0)',
|
||||
labelsFit: true,
|
||||
styleNonce: RUNTIME_STYLE_NONCE,
|
||||
text: expect.stringContaining('Linked to a planned shift?'),
|
||||
})
|
||||
})
|
||||
@@ -283,6 +293,7 @@ ${SYSTEM_OVERVIEW_DIAGRAM}
|
||||
scheduledStroke: 'rgb(122, 91, 0)',
|
||||
scheduledTextColor: 'rgb(58, 44, 0)',
|
||||
labelsFit: true,
|
||||
styleNonce: RUNTIME_STYLE_NONCE,
|
||||
text: expect.stringContaining('Linked to a planned shift?'),
|
||||
})
|
||||
await expect(page.locator('[data-testid="mermaid-diagram-error"]')).toHaveCount(1)
|
||||
|
||||
Reference in New Issue
Block a user