math-rendering.e2e.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { fileURLToPath } from 'node:url'
  2. import type { Browser, Page } from 'playwright'
  3. import { chromium } from 'playwright'
  4. import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
  5. import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
  6. import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
  7. import type {} from '@deepseek-ai/dsh-session-title'
  8. import {
  9. assertFixtureInventory,
  10. captureStableAria,
  11. compareOrRefreshGolden,
  12. launchWebScaffold,
  13. seedSession,
  14. watchConsole,
  15. webSnapshotMode,
  16. type WebScaffold,
  17. } from './scaffold.ts'
  18. import { newEnglishPage, saveFailureShot } from './support.ts'
  19. const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/math-rendering', import.meta.url))
  20. const UI_EXPECTED = fileURLToPath(new URL('./expected/math-rendering/ui.expected.md', import.meta.url))
  21. const MODE = webSnapshotMode()
  22. const SEED_ID = 'math-rendering-web-e2e'
  23. const DONE = 'MATH_RENDERING_DONE'
  24. /** Build a settled assistant reply that exercises every supported math delimiter. */
  25. function mathFixture(): string {
  26. const session = Session.create(SessionId('math-rendering-source'))
  27. const eventTimeOrigin = new Date().setHours(12, 0, 0, 0)
  28. session.append('turn/start', {
  29. turn: 1,
  30. })
  31. const user = session.append('user/message', createUserMessage({
  32. content: [{ type: 'text', text: 'Render this mathematical proof.' }],
  33. source: { kind: 'user' },
  34. }), { surfaceOp: 'append' })
  35. session.append('session/title', {
  36. title: 'Math rendering',
  37. messageSeqs: [user.seq],
  38. source: { kind: 'fallback' },
  39. })
  40. session.append('step/start', { turn: 1, step: 1 })
  41. session.append('assistant/message', {
  42. stream: [],
  43. turn: 1,
  44. step: 1,
  45. message: createMessage({
  46. role: 'assistant',
  47. content: [{
  48. type: 'text',
  49. text: [
  50. '## Math rendering',
  51. '',
  52. 'Inline dollar $\\theta$ and backslash \\(\\frac{1}{5}\\).',
  53. '',
  54. '\\[\\frac{\\pi}{4} < \\theta < \\frac{\\pi}{2}\\]',
  55. '',
  56. '$$\\theta \\in \\left(\\frac{\\pi}{4}, \\frac{\\pi}{2}\\right). \\tag{1}$$',
  57. '',
  58. '| Symbol | Value |',
  59. '| --- | --- |',
  60. '| $\\theta$ | \\(\\frac{1}{5}\\) |',
  61. '',
  62. DONE,
  63. ].join('\n'),
  64. }],
  65. source: { kind: 'model', provider: 'fixture', model: 'fixture' },
  66. }),
  67. }, { surfaceOp: 'append' })
  68. session.append('step/end', { turn: 1, step: 1 })
  69. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  70. return [
  71. JSON.stringify({
  72. type: 'session',
  73. version: SESSION_FORMAT_VERSION,
  74. id: '{{sessionId}}',
  75. createdAt: 0,
  76. cwd: '{{cwd}}',
  77. isSeeded: false,
  78. delegationDepth: 0,
  79. }),
  80. ...session.snapshotEvents().map(event => JSON.stringify({
  81. ...event,
  82. time: eventTimeOrigin + event.seq * 1_000,
  83. })),
  84. '',
  85. ].join('\n')
  86. }
  87. describe('web e2e: settled Markdown math rendering', () => {
  88. let scaffold: WebScaffold
  89. let browser: Browser
  90. let page: Page
  91. let tripwire: ReturnType<typeof watchConsole>
  92. beforeAll(async () => {
  93. scaffold = await launchWebScaffold({})
  94. await seedSession(scaffold, mathFixture(), SEED_ID)
  95. browser = await chromium.launch()
  96. page = await newEnglishPage(browser)
  97. tripwire = watchConsole(page)
  98. await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
  99. await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
  100. }, 120_000)
  101. afterAll(async () => {
  102. await browser?.close()
  103. await scaffold?.close()
  104. })
  105. it.skipIf(MODE === 'record')('renders the settled reply without KaTeX errors', async () => {
  106. onTestFailed(() => saveFailureShot(page, 'web-e2e-math-rendering'))
  107. const groupRow = page.locator('[role="treeitem"]').first()
  108. await groupRow.waitFor({ timeout: 15_000 })
  109. await groupRow.click()
  110. const sessionRow = page.locator('[role="treeitem"]').nth(1)
  111. await sessionRow.waitFor({ timeout: 10_000 })
  112. await sessionRow.click()
  113. await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
  114. await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(6)
  115. await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2)
  116. expect(await page.locator('.katex-error').count()).toBe(0)
  117. await expect.poll(
  118. () => page.getByText('1 turns · 1 steps', { exact: false }).count(),
  119. { timeout: 10_000 },
  120. ).toBe(1)
  121. const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
  122. .split(SEED_ID).join('{{seededId}}')
  123. await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
  124. expect(tripwire.pageErrors).toEqual([])
  125. expect(tripwire.warnings).toEqual([])
  126. await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
  127. }, 60_000)
  128. })