markdown-cjk-strong.e2e.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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/markdown-cjk-strong', import.meta.url))
  20. const UI_EXPECTED = fileURLToPath(new URL('./expected/markdown-cjk-strong/ui.expected.md', import.meta.url))
  21. const MODE = webSnapshotMode()
  22. const SEED_ID = 'markdown-cjk-strong-web-e2e'
  23. const DONE = 'CJK_STRONG_DONE'
  24. const CASES = [
  25. ['**注意:**内容', '注意:', '注意:内容'],
  26. ['**Notice:**内容', 'Notice:', 'Notice:内容'],
  27. ['**事件中间件(waterfall)**实现', '事件中间件(waterfall)', '事件中间件(waterfall)实现'],
  28. ['**事件中间件(waterfall)**实现', '事件中间件(waterfall)', '事件中间件(waterfall)实现'],
  29. ['**句号。**后续', '句号。', '句号。后续'],
  30. ['**Period.**后续', 'Period.', 'Period.后续'],
  31. ['**提醒!**继续', '提醒!', '提醒!继续'],
  32. ['**Warning!**继续', 'Warning!', 'Warning!继续'],
  33. ] as const
  34. /** Build one settled assistant reply covering CJK-adjacent strong punctuation boundaries. */
  35. function markdownFixture(): string {
  36. const session = Session.create(SessionId('markdown-cjk-strong-source'))
  37. const eventTimeOrigin = new Date().setHours(12, 0, 0, 0)
  38. session.append('turn/start', { turn: 1 })
  39. const user = session.append('user/message', createUserMessage({
  40. content: [{ type: 'text', text: 'Render adjacent CJK strong emphasis.' }],
  41. source: { kind: 'user' },
  42. }), { surfaceOp: 'append' })
  43. session.append('session/title', {
  44. title: 'CJK strong emphasis',
  45. messageSeqs: [user.seq],
  46. source: { kind: 'fallback' },
  47. })
  48. session.append('step/start', { turn: 1, step: 1 })
  49. session.append('assistant/message', {
  50. stream: [],
  51. turn: 1,
  52. step: 1,
  53. message: createMessage({
  54. role: 'assistant',
  55. content: [{
  56. type: 'text',
  57. text: [
  58. '## CJK strong emphasis',
  59. '',
  60. ...CASES.flatMap(([markdown]) => [markdown, '']),
  61. DONE,
  62. ].join('\n'),
  63. }],
  64. source: { kind: 'model', provider: 'fixture', model: 'fixture' },
  65. }),
  66. }, { surfaceOp: 'append' })
  67. session.append('step/end', { turn: 1, step: 1 })
  68. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  69. return [
  70. JSON.stringify({
  71. type: 'session',
  72. version: SESSION_FORMAT_VERSION,
  73. id: '{{sessionId}}',
  74. createdAt: 0,
  75. cwd: '{{cwd}}',
  76. isSeeded: false,
  77. delegationDepth: 0,
  78. }),
  79. ...session.snapshotEvents().map(event => JSON.stringify({
  80. ...event,
  81. time: eventTimeOrigin + event.seq * 1_000,
  82. })),
  83. '',
  84. ].join('\n')
  85. }
  86. describe('web e2e: CJK-adjacent Markdown strong emphasis', () => {
  87. let scaffold: WebScaffold
  88. let browser: Browser
  89. let page: Page
  90. let tripwire: ReturnType<typeof watchConsole>
  91. beforeAll(async () => {
  92. scaffold = await launchWebScaffold({})
  93. await seedSession(scaffold, markdownFixture(), SEED_ID)
  94. browser = await chromium.launch()
  95. page = await newEnglishPage(browser)
  96. tripwire = watchConsole(page)
  97. await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
  98. await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
  99. }, 120_000)
  100. afterAll(async () => {
  101. await browser?.close()
  102. await scaffold?.close()
  103. })
  104. it.skipIf(MODE === 'record')('renders punctuation-terminated strong spans before adjacent CJK text', async () => {
  105. onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-cjk-strong'))
  106. const groupRow = page.locator('[role="treeitem"]').first()
  107. await groupRow.waitFor({ timeout: 15_000 })
  108. await groupRow.click()
  109. const sessionRow = page.locator('[role="treeitem"]').nth(1)
  110. await sessionRow.waitFor({ timeout: 10_000 })
  111. await sessionRow.click()
  112. await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
  113. const strong = page.locator('[class*="markdown"] strong')
  114. await expect.poll(() => strong.count(), { timeout: 10_000 }).toBe(CASES.length)
  115. expect(await strong.allTextContents()).toEqual(CASES.map(([, expected]) => expected))
  116. for (const [, , paragraph] of CASES) {
  117. expect(await page.getByText(paragraph, { exact: true }).count()).toBe(1)
  118. }
  119. const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
  120. .split(SEED_ID).join('{{seededId}}')
  121. await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
  122. expect(tripwire.pageErrors).toEqual([])
  123. expect(tripwire.warnings).toEqual([])
  124. await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
  125. }, 60_000)
  126. })