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 ? `${content}` : `${content}` }, } const browserKeys = [ 'window', 'document', 'navigator', 'self', 'Element', 'HTMLElement', 'SVGElement', 'Node', 'Text', 'Comment', 'Document', 'MutationObserver', 'Event', 'CustomEvent', 'getComputedStyle', 'requestAnimationFrame', 'cancelAnimationFrame', ] as const async function withGlobalOverrides(values: Partial>, run: () => Promise) { const previous = new Map() 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>, 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 = `
${serverHtml}
` 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) }) })