Files
BoosAPI/desktop/libs/markstream-vue/test/math-block-hydration.test.ts
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

134 lines
3.6 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
const mathNode = {
type: 'math_block',
content: '\\sum_{n=1}^{3} n = 6',
raw: '$$\\sum_{n=1}^{3} n = 6$$',
markup: '$$',
loading: false,
} as const
const katexRenderer = {
renderToString(content: string, options?: { displayMode?: boolean }) {
return options?.displayMode
? `<span class="katex-display">${content}</span>`
: `<span class="katex">${content}</span>`
},
}
const browserKeys = [
'window',
'document',
'navigator',
'self',
'Element',
'HTMLElement',
'SVGElement',
'Node',
'Text',
'Comment',
'Document',
'MutationObserver',
'Event',
'CustomEvent',
'getComputedStyle',
'requestAnimationFrame',
'cancelAnimationFrame',
] as const
async function withGlobalOverrides<T>(values: Partial<Record<(typeof browserKeys)[number], unknown>>, run: () => Promise<T>) {
const previous = new Map<string, PropertyDescriptor | undefined>()
for (const key of browserKeys) {
previous.set(key, Object.getOwnPropertyDescriptor(globalThis, key))
if (!(key in values))
continue
Object.defineProperty(globalThis, key, {
configurable: true,
writable: true,
value: values[key],
})
}
try {
return await run()
}
finally {
for (const [key, descriptor] of previous) {
if (descriptor) {
Object.defineProperty(globalThis, key, descriptor)
}
else {
Reflect.deleteProperty(globalThis, key)
}
}
}
}
async function renderServerMathBlock() {
return withGlobalOverrides(
Object.fromEntries(browserKeys.map(key => [key, undefined])) as Partial<Record<(typeof browserKeys)[number], undefined>>,
async () => {
vi.resetModules()
const vue = await import('vue')
const { renderToString } = await import('vue/server-renderer')
const { setKatexLoader } = await import('../src/components/MathInlineNode/katex')
const MathBlockNode = (await import('../src/components/MathBlockNode/MathBlockNode.vue')).default
setKatexLoader(() => katexRenderer)
const app = vue.createSSRApp({
render: () => vue.h(MathBlockNode, { node: mathNode }),
})
return renderToString(app)
},
)
}
describe('mathBlockNode hydration', () => {
afterEach(() => {
document.body.innerHTML = ''
vi.restoreAllMocks()
vi.resetModules()
})
it('hydrates server-rendered KaTeX markup without mismatch warnings', async () => {
const serverHtml = await renderServerMathBlock()
expect(serverHtml).toContain('data-markstream-mode="katex"')
expect(serverHtml).toContain('katex-display')
document.body.innerHTML = `<div id="app">${serverHtml}</div>`
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
vi.resetModules()
const vue = await import('vue')
const { setKatexLoader } = await import('../src/components/MathInlineNode/katex')
const MathBlockNode = (await import('../src/components/MathBlockNode/MathBlockNode.vue')).default
setKatexLoader(() => katexRenderer)
const app = vue.createSSRApp({
render: () => vue.h(MathBlockNode, { node: mathNode }),
})
const container = document.querySelector('#app') as HTMLElement
app.mount(container)
await vue.nextTick()
await Promise.resolve()
const diagnostics = [
...consoleError.mock.calls.flat(),
...consoleWarn.mock.calls.flat(),
].join(' ')
expect(container.innerHTML).toContain('katex-display')
expect(diagnostics).not.toMatch(/Hydration/i)
expect(diagnostics).not.toMatch(/mismatch/i)
})
})