A `$$math` block whose opening line carries math and whose closing `$$` sits at the end of the last math line parsed as an unclosed flow: micromark-extension-math reads the opening line's text as fence meta and accepts a closing fence only on a line whose content is exactly `$$`. The flow then consumed the following document, and KaTeX rendered the block plus that swallowed text as one error span. Fence meta was also silently dropped because the renderer reads only the math node's value. The compatibility layer's dollar flow now owns every `$$` fence, spans multiple lines, and stops its paragraph-interruption check before the line ending, as the upstream flow does; registering it after `math()` is what gives it that precedence in micromark.
141 lines
4.9 KiB
TypeScript
141 lines
4.9 KiB
TypeScript
import { fileURLToPath } from 'node:url'
|
|
import type { Browser, Page } from 'playwright'
|
|
import { chromium } from 'playwright'
|
|
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
|
import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
|
|
import type {} from '@deepseek-ai/dsh-session-title'
|
|
import {
|
|
assertFixtureInventory,
|
|
captureStableAria,
|
|
compareOrRefreshGolden,
|
|
launchWebScaffold,
|
|
seedSession,
|
|
watchConsole,
|
|
webSnapshotMode,
|
|
type WebScaffold,
|
|
} from './scaffold.ts'
|
|
import { newEnglishPage, saveFailureShot } from './support.ts'
|
|
|
|
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/math-rendering', import.meta.url))
|
|
const UI_EXPECTED = fileURLToPath(new URL('./expected/math-rendering/ui.expected.md', import.meta.url))
|
|
const MODE = webSnapshotMode()
|
|
const SEED_ID = 'math-rendering-web-e2e'
|
|
const DONE = 'MATH_RENDERING_DONE'
|
|
|
|
/** Build a settled assistant reply that exercises every supported math delimiter. */
|
|
function mathFixture(): string {
|
|
const session = Session.create(SessionId('math-rendering-source'))
|
|
const eventTimeOrigin = new Date().setHours(12, 0, 0, 0)
|
|
session.append('turn/start', {
|
|
turn: 1,
|
|
})
|
|
const user = session.append('user/message', createUserMessage({
|
|
content: [{ type: 'text', text: 'Render this mathematical proof.' }],
|
|
source: { kind: 'user' },
|
|
}), { surfaceOp: 'append' })
|
|
session.append('session/title', {
|
|
title: 'Math rendering',
|
|
messageSeqs: [user.seq],
|
|
source: { kind: 'fallback' },
|
|
})
|
|
session.append('step/start', { turn: 1, step: 1 })
|
|
session.append('assistant/message', {
|
|
stream: [],
|
|
turn: 1,
|
|
step: 1,
|
|
message: createMessage({
|
|
role: 'assistant',
|
|
content: [{
|
|
type: 'text',
|
|
text: [
|
|
'## Math rendering',
|
|
'',
|
|
'Inline dollar $\\theta$ and backslash \\(\\frac{1}{5}\\).',
|
|
'',
|
|
'\\[\\frac{\\pi}{4} < \\theta < \\frac{\\pi}{2}\\]',
|
|
'',
|
|
'$$\\theta \\in \\left(\\frac{\\pi}{4}, \\frac{\\pi}{2}\\right). \\tag{1}$$',
|
|
'',
|
|
'$$\\alpha+\\beta',
|
|
'\\gamma$$',
|
|
'',
|
|
'| Symbol | Value |',
|
|
'| --- | --- |',
|
|
'| $\\theta$ | \\(\\frac{1}{5}\\) |',
|
|
'',
|
|
DONE,
|
|
].join('\n'),
|
|
}],
|
|
source: { kind: 'model', provider: 'fixture', model: 'fixture' },
|
|
}),
|
|
}, { surfaceOp: 'append' })
|
|
session.append('step/end', { turn: 1, step: 1 })
|
|
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
|
|
return [
|
|
JSON.stringify({
|
|
type: 'session',
|
|
version: SESSION_FORMAT_VERSION,
|
|
id: '{{sessionId}}',
|
|
createdAt: 0,
|
|
cwd: '{{cwd}}',
|
|
isSeeded: false,
|
|
delegationDepth: 0,
|
|
}),
|
|
...session.snapshotEvents().map(event => JSON.stringify({
|
|
...event,
|
|
time: eventTimeOrigin + event.seq * 1_000,
|
|
})),
|
|
'',
|
|
].join('\n')
|
|
}
|
|
|
|
describe('web e2e: settled Markdown math rendering', () => {
|
|
let scaffold: WebScaffold
|
|
let browser: Browser
|
|
let page: Page
|
|
let tripwire: ReturnType<typeof watchConsole>
|
|
|
|
beforeAll(async () => {
|
|
scaffold = await launchWebScaffold({})
|
|
await seedSession(scaffold, mathFixture(), SEED_ID)
|
|
browser = await chromium.launch()
|
|
page = await newEnglishPage(browser)
|
|
tripwire = watchConsole(page)
|
|
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
|
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
|
}, 120_000)
|
|
|
|
afterAll(async () => {
|
|
await browser?.close()
|
|
await scaffold?.close()
|
|
})
|
|
|
|
it.skipIf(MODE === 'record')('renders the settled reply without KaTeX errors', async () => {
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-math-rendering'))
|
|
const groupRow = page.locator('[role="treeitem"]').first()
|
|
await groupRow.waitFor({ timeout: 15_000 })
|
|
await groupRow.click()
|
|
const sessionRow = page.locator('[role="treeitem"]').nth(1)
|
|
await sessionRow.waitFor({ timeout: 10_000 })
|
|
await sessionRow.click()
|
|
await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
|
|
|
|
await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(7)
|
|
await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(3)
|
|
expect(await page.locator('.katex-error').count()).toBe(0)
|
|
await expect.poll(
|
|
() => page.getByText('1 turns 1 steps', { exact: false }).count(),
|
|
{ timeout: 10_000 },
|
|
).toBe(1)
|
|
|
|
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
|
.split(SEED_ID).join('{{seededId}}')
|
|
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
|
expect(tripwire.pageErrors).toEqual([])
|
|
expect(tripwire.warnings).toEqual([])
|
|
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
|
|
}, 60_000)
|
|
})
|