✨ feat(vscode): 代码阅读预览把注释、说明、问 AI 回答里的 mermaid 代码块画成图
- markdown.ts:mermaid 代码块出占位(源码转义后记在 data-source,里面先放原样的代码块);带增删的说明、逐字生成中的回答仍是普通代码块 - useMermaidDiagrams:页面上有占位才按需加载 mermaid(strict、跟 VS Code 字体),按源码 + 配色缓存 SVG,一次只画一张,换主题重画;画不出来留代码块、下面一行原因 - 打包:Vite 的按需加载辅助函数单独成块,加构建检查禁止别的块 import 入口(入口带 ?v= 加载,被 import 会整页再跑一遍、第二次拿 VS Code API 报错) - 主题明暗判断抽到 lib/vscodeTheme,看板的改动面板共用
This commit is contained in:
1 parent
bc84e74f29
commit
6dac457ed1
11 files changed
+1289
-24
No files matched your search
@@ -0,0 +1,163 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { Root } from 'react-dom/client'
|
||||
import { act } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { renderMarkdown } from './markdown'
|
||||
|
||||
/** 假的 mermaid:记下加载了几次、按哪个主题初始化、画了什么;源码里有 `坏` 就当解析失败。 */
|
||||
const fake = vi.hoisted(() => ({
|
||||
imports: 0,
|
||||
theme: '',
|
||||
running: 0,
|
||||
mostRunning: 0,
|
||||
/** 每张图画之前等它,测并发时卡住 */
|
||||
gate: Promise.resolve(),
|
||||
initialize: vi.fn((config: { theme: string }) => {
|
||||
fake.theme = config.theme
|
||||
}),
|
||||
render: vi.fn(async (id: string, text: string) => {
|
||||
fake.running += 1
|
||||
fake.mostRunning = Math.max(fake.mostRunning, fake.running)
|
||||
await fake.gate
|
||||
fake.running -= 1
|
||||
if (text.includes('坏'))
|
||||
throw new Error('Parse error on line 2:\n坏\n^\nExpecting \'NODE\', got \'EOF\'')
|
||||
return { svg: `<svg id="${id}" data-theme="${fake.theme}"><text>${text.trim()}</text></svg>` }
|
||||
}),
|
||||
}))
|
||||
|
||||
/** 加载一次 mermaid 记一次 */
|
||||
function mermaidModule(): { default: Pick<typeof fake, 'initialize' | 'render'> } {
|
||||
fake.imports += 1
|
||||
return { default: { initialize: fake.initialize, render: fake.render } }
|
||||
}
|
||||
|
||||
let root: Root
|
||||
let page: HTMLDivElement
|
||||
|
||||
/** 挂上只用这个 hook 的组件;每个测试重新加载模块、重新登记假的 mermaid,mermaid 有没有被加载各算各的 */
|
||||
async function mount(): Promise<void> {
|
||||
vi.resetModules()
|
||||
vi.doMock('mermaid', mermaidModule)
|
||||
const { useMermaidDiagrams } = await import('./useMermaidDiagrams')
|
||||
function Diagrams(): null {
|
||||
useMermaidDiagrams()
|
||||
return null
|
||||
}
|
||||
const host = document.createElement('div')
|
||||
document.body.append(host)
|
||||
root = createRoot(host)
|
||||
await act(async () => root.render(<Diagrams />))
|
||||
}
|
||||
|
||||
/** 页面上加一段注释的 Markdown,返回它的容器 */
|
||||
function addMarkdown(markdown: string): HTMLDivElement {
|
||||
const div = document.createElement('div')
|
||||
div.className = 'cr-markdown'
|
||||
div.innerHTML = renderMarkdown(markdown)
|
||||
page.append(div)
|
||||
return div
|
||||
}
|
||||
|
||||
const flow = (text: string): string => `\`\`\`mermaid\nflowchart TD\n ${text}\n\`\`\``
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
||||
document.body.className = 'vscode-dark'
|
||||
page = document.createElement('div')
|
||||
document.body.append(page)
|
||||
fake.gate = Promise.resolve()
|
||||
fake.running = 0
|
||||
fake.mostRunning = 0
|
||||
fake.render.mockClear()
|
||||
fake.initialize.mockClear()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
document.body.replaceChildren()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('useMermaidDiagrams', () => {
|
||||
it('页面上没有 mermaid 占位就不加载 mermaid', async () => {
|
||||
const before = fake.imports
|
||||
await mount()
|
||||
addMarkdown('```ts\nload()\n```\n\n说明')
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
expect(fake.imports).toBe(before)
|
||||
|
||||
addMarkdown(flow('A["开始"] --> B["结束"]'))
|
||||
await vi.waitFor(() => expect(page.querySelector('.cr-mermaid svg')).not.toBeNull())
|
||||
expect(fake.imports).toBe(before + 1)
|
||||
})
|
||||
|
||||
it('只画占位:代码块换成图,别的代码块不动;初始化不跑页面脚本、深色主题', async () => {
|
||||
await mount()
|
||||
const other = addMarkdown('```ts\nload()\n```')
|
||||
const diagram = addMarkdown(flow('A["读取会话"] --> B{"是本人的?"}'))
|
||||
await vi.waitFor(() => expect(diagram.querySelector('.cr-mermaid svg')).not.toBeNull())
|
||||
expect(diagram.querySelector('.cr-mermaid pre')).toBeNull()
|
||||
expect(diagram.querySelector('svg')!.dataset.theme).toBe('dark')
|
||||
expect(other.innerHTML).toBe('<pre><code class="language-ts">load()\n</code></pre>\n')
|
||||
expect(fake.render).toHaveBeenCalledTimes(1)
|
||||
expect(fake.render.mock.calls[0][1]).toBe('flowchart TD\n A["读取会话"] --> B{"是本人的?"}\n')
|
||||
expect(fake.initialize).toHaveBeenCalledWith(expect.objectContaining({ startOnLoad: false, securityLevel: 'strict', theme: 'dark' }))
|
||||
})
|
||||
|
||||
it('同样的源码、同样的主题只画一次:两处一起画,卡片重新挂上直接用画好的', async () => {
|
||||
await mount()
|
||||
const first = addMarkdown(flow('A --> B'))
|
||||
const second = addMarkdown(flow('A --> B'))
|
||||
await vi.waitFor(() => expect(second.querySelector('.cr-mermaid svg')).not.toBeNull())
|
||||
expect(first.querySelector('.cr-mermaid svg')).not.toBeNull()
|
||||
expect(fake.render).toHaveBeenCalledTimes(1)
|
||||
|
||||
first.remove()
|
||||
second.remove()
|
||||
const again = addMarkdown(flow('A --> B'))
|
||||
await vi.waitFor(() => expect(again.querySelector('.cr-mermaid svg')).not.toBeNull())
|
||||
expect(fake.render).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('主题变了按新主题重画;换回来用画过的', async () => {
|
||||
await mount()
|
||||
const diagram = addMarkdown(flow('A --> B'))
|
||||
await vi.waitFor(() => expect(diagram.querySelector('svg')?.dataset.theme).toBe('dark'))
|
||||
|
||||
document.body.className = 'vscode-light'
|
||||
await vi.waitFor(() => expect(diagram.querySelector('svg')?.dataset.theme).toBe('default'))
|
||||
expect(fake.render).toHaveBeenCalledTimes(2)
|
||||
|
||||
document.body.className = 'vscode-high-contrast'
|
||||
await vi.waitFor(() => expect(diagram.querySelector('svg')?.dataset.theme).toBe('dark'))
|
||||
expect(fake.render).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('画不出来:留着代码块,下面写一行简短原因,悬停看全文', async () => {
|
||||
await mount()
|
||||
const diagram = addMarkdown(flow('坏 -->'))
|
||||
await vi.waitFor(() => expect(diagram.querySelector('.cr-mermaid-error')).not.toBeNull())
|
||||
expect(diagram.querySelector('.cr-mermaid pre code')!.textContent).toBe('flowchart TD\n 坏 -->\n')
|
||||
expect(diagram.querySelector('svg')).toBeNull()
|
||||
const error = diagram.querySelector<HTMLElement>('.cr-mermaid-error')!
|
||||
expect(error.textContent).toBe('图画不出来:Parse error on line 2:')
|
||||
expect(error.dataset.tooltip).toContain('Expecting \'NODE\', got \'EOF\'')
|
||||
})
|
||||
|
||||
it('一次只画一张', async () => {
|
||||
let open = (): void => {}
|
||||
fake.gate = new Promise(resolve => open = resolve)
|
||||
await mount()
|
||||
const diagrams = [addMarkdown(flow('A --> B')), addMarkdown(flow('C --> D')), addMarkdown(flow('E --> F'))]
|
||||
await vi.waitFor(() => expect(fake.render).toHaveBeenCalledTimes(1))
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
expect(fake.render).toHaveBeenCalledTimes(1)
|
||||
open()
|
||||
await vi.waitFor(() => expect(diagrams.every(diagram => diagram.querySelector('svg'))).toBe(true))
|
||||
expect(fake.render).toHaveBeenCalledTimes(3)
|
||||
expect(fake.mostRunning).toBe(1)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user