157 lines
5.3 KiB
TypeScript
157 lines
5.3 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { createElement } from 'react'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import { MermaidSvgCache } from '../src/renderer/src/workbuddy/mermaid/cache'
|
|
import { MermaidBlock } from '../src/renderer/src/workbuddy/mermaid/MermaidBlock'
|
|
import {
|
|
createMermaidRenderer,
|
|
type MermaidApi,
|
|
type MermaidConfiguration
|
|
} from '../src/renderer/src/workbuddy/mermaid/renderer'
|
|
import {
|
|
isWidthDependentDiagram,
|
|
normalizeMermaidCode
|
|
} from '../src/renderer/src/workbuddy/mermaid/sanitize'
|
|
import { remapSvgIdReferences } from '../src/renderer/src/workbuddy/mermaid/svg'
|
|
import {
|
|
MERMAID_ACTIVATION_ROOT_MARGIN,
|
|
MERMAID_VISIBLE_ROOT_MARGIN
|
|
} from '../src/renderer/src/workbuddy/mermaid/visibility'
|
|
import {
|
|
MERMAID_WIDTH_DEBOUNCE_MS,
|
|
shouldPublishMermaidWidth
|
|
} from '../src/renderer/src/workbuddy/mermaid/width'
|
|
|
|
const flowchart = normalizeMermaidCode('flowchart LR\nA["draft (one)"] --> B[\'two\']')
|
|
assert.equal(flowchart, 'flowchart LR\nA[draft one] --> B[two]')
|
|
|
|
const graph = normalizeMermaidCode('graph TD\nA["one"] --> B[\'two\']\nclick A "open \"details\""')
|
|
assert.match(graph, /A\[one\] --> B\[two\]/)
|
|
assert.ok(graph.includes("click A \"open 'details'\""))
|
|
|
|
const xychart = normalizeMermaidCode('xychart-beta\nx-axis "Quarter (Q1)" ["One", "Two"]')
|
|
assert.match(xychart, /x-axis ".*Quarter.*Q1.*" \[/)
|
|
assert.match(xychart, /_#40#_.*_#41#_/)
|
|
assert.equal(isWidthDependentDiagram('---\ntitle: Plan\n---\n%% note\ngantt\ndateFormat YYYY-MM-DD'), true)
|
|
assert.equal(isWidthDependentDiagram('flowchart LR\nA-->B'), false)
|
|
assert.equal(MERMAID_VISIBLE_ROOT_MARGIN, '320px')
|
|
assert.equal(MERMAID_ACTIVATION_ROOT_MARGIN, '320px 0px')
|
|
assert.equal(MERMAID_WIDTH_DEBOUNCE_MS, 360)
|
|
assert.equal(shouldPublishMermaidWidth(0, 1), true)
|
|
assert.equal(shouldPublishMermaidWidth(500, 515), false)
|
|
assert.equal(shouldPublishMermaidWidth(500, 516), true)
|
|
|
|
const ids = new Map([
|
|
['a', 'cr-mermaid-1-a'],
|
|
['long-id', 'cr-mermaid-1-long-id']
|
|
])
|
|
assert.equal(
|
|
remapSvgIdReferences('fill="url(#a)" href="#long-id" url(#a-next)', ids),
|
|
'fill="url(#cr-mermaid-1-a)" href="#cr-mermaid-1-long-id" url(#a-next)'
|
|
)
|
|
assert.equal(
|
|
remapSvgIdReferences('url(#a.b) url(#axb)', new Map([['a.b', 'safe-a-b']])),
|
|
'url(#safe-a-b) url(#axb)'
|
|
)
|
|
|
|
const lru = new MermaidSvgCache(1024, 2)
|
|
lru.set('one', '<svg>one</svg>')
|
|
lru.set('two', '<svg>two</svg>')
|
|
assert.equal(lru.get('one'), '<svg>one</svg>')
|
|
lru.set('three', '<svg>three</svg>')
|
|
assert.equal(lru.get('two'), undefined)
|
|
assert.equal(lru.size, 2)
|
|
assert.ok(lru.byteSize <= 1024)
|
|
|
|
const entryBounded = new MermaidSvgCache()
|
|
for (let index = 0; index < 65; index++) entryBounded.set(`diagram-${index}`, '<svg/>')
|
|
assert.equal(entryBounded.size, 64)
|
|
assert.equal(entryBounded.get('diagram-0'), undefined)
|
|
|
|
const oversized = new MermaidSvgCache(8, 4)
|
|
oversized.set('too-large-key', '<svg/>')
|
|
assert.equal(oversized.size, 0)
|
|
|
|
let activeRenders = 0
|
|
let maxActiveRenders = 0
|
|
const renderedCodes: string[] = []
|
|
const configurations: MermaidConfiguration[] = []
|
|
const fakeMermaid: MermaidApi = {
|
|
initialize(configuration) { configurations.push(configuration) },
|
|
async render(_id, code) {
|
|
activeRenders++
|
|
maxActiveRenders = Math.max(maxActiveRenders, activeRenders)
|
|
renderedCodes.push(code)
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
activeRenders--
|
|
return { svg: `<svg>${code}</svg>` }
|
|
}
|
|
}
|
|
|
|
const render = createMermaidRenderer()
|
|
const firstController = new AbortController()
|
|
const secondController = new AbortController()
|
|
const firstRender = render({
|
|
mermaid: fakeMermaid,
|
|
code: 'gantt\ntitle first',
|
|
dark: true,
|
|
width: 540,
|
|
signal: firstController.signal
|
|
})
|
|
const secondRender = render({
|
|
mermaid: fakeMermaid,
|
|
code: 'flowchart LR\nA-->B',
|
|
dark: false,
|
|
width: 540,
|
|
signal: secondController.signal
|
|
})
|
|
secondController.abort()
|
|
assert.equal(await firstRender, '<svg>gantt\ntitle first</svg>')
|
|
assert.equal(await secondRender, undefined)
|
|
assert.deepEqual(renderedCodes, ['gantt\ntitle first'])
|
|
assert.equal(maxActiveRenders, 1)
|
|
assert.equal(configurations[0].gantt.useWidth, 540)
|
|
|
|
const cacheHitController = new AbortController()
|
|
assert.equal(await render({
|
|
mermaid: fakeMermaid,
|
|
code: 'gantt\ntitle first',
|
|
dark: true,
|
|
width: 540,
|
|
signal: cacheHitController.signal
|
|
}), '<svg>gantt\ntitle first</svg>')
|
|
assert.equal(renderedCodes.length, 1)
|
|
|
|
let releaseSlowRender: ((value: string) => void) | undefined
|
|
let slowStarted: (() => void) | undefined
|
|
const slowApi: MermaidApi = {
|
|
initialize() {},
|
|
render() {
|
|
slowStarted?.()
|
|
return new Promise<string>((resolve) => { releaseSlowRender = resolve })
|
|
}
|
|
}
|
|
const staleController = new AbortController()
|
|
const staleRenderer = createMermaidRenderer()
|
|
const staleResult = staleRenderer({
|
|
mermaid: slowApi,
|
|
code: 'flowchart LR\nA-->B',
|
|
dark: false,
|
|
width: 800,
|
|
signal: staleController.signal
|
|
})
|
|
await new Promise<void>((resolve) => { slowStarted = resolve })
|
|
staleController.abort()
|
|
releaseSlowRender?.('<svg>stale</svg>')
|
|
assert.equal(await staleResult, undefined)
|
|
|
|
const blockMarkup = renderToStaticMarkup(createElement(MermaidBlock, {
|
|
code: 'flowchart LR\nA-->B',
|
|
isRendering: false
|
|
}))
|
|
assert.match(blockMarkup, /cr-mermaid-block__tab--active/)
|
|
assert.match(blockMarkup, />图表</)
|
|
assert.match(blockMarkup, /data-mermaid-render-state="pending"/)
|
|
|
|
console.log('Mermaid cache, sanitizer, queue, and abort regression checks passed')
|