markdown-inline-code-links.e2e.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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-inline-code-links', import.meta.url))
  20. const UI_EXPECTED = fileURLToPath(new URL('./expected/markdown-inline-code-links/ui.expected.md', import.meta.url))
  21. const MODE = webSnapshotMode()
  22. const SEED_ID = 'markdown-inline-code-links-web-e2e'
  23. const DONE = 'INLINE_CODE_LINK_DONE'
  24. const LINK_URL = 'http://127.0.0.1:3199/?demo=1'
  25. /** Build a settled assistant reply with linkable URL code and inert code controls. */
  26. function markdownFixture(linkUrl: string): string {
  27. const session = Session.create(SessionId('markdown-inline-code-links-source'))
  28. const eventTimeOrigin = new Date().setHours(12, 0, 0, 0)
  29. session.append('turn/start', { turn: 1 })
  30. const user = session.append('user/message', createUserMessage({
  31. content: [{ type: 'text', text: 'Show the local preview URL.' }],
  32. source: { kind: 'user' },
  33. }), { surfaceOp: 'append' })
  34. session.append('session/title', {
  35. title: 'Inline code links',
  36. messageSeqs: [user.seq],
  37. source: { kind: 'fallback' },
  38. })
  39. session.append('step/start', { turn: 1, step: 1 })
  40. session.append('assistant/message', {
  41. stream: [],
  42. turn: 1,
  43. step: 1,
  44. message: createMessage({
  45. role: 'assistant',
  46. content: [{
  47. type: 'text',
  48. text: [
  49. '## Inline code links',
  50. '',
  51. `Preview: \`${linkUrl}\``,
  52. '',
  53. `Standard: [Open preview](${linkUrl})`,
  54. '',
  55. `Command: \`curl ${linkUrl}\``,
  56. '',
  57. 'Unsafe: `javascript:alert(1)`',
  58. '',
  59. DONE,
  60. ].join('\n'),
  61. }],
  62. source: { kind: 'model', provider: 'fixture', model: 'fixture' },
  63. }),
  64. }, { surfaceOp: 'append' })
  65. session.append('step/end', { turn: 1, step: 1 })
  66. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  67. return [
  68. JSON.stringify({
  69. type: 'session',
  70. version: SESSION_FORMAT_VERSION,
  71. id: '{{sessionId}}',
  72. createdAt: 0,
  73. cwd: '{{cwd}}',
  74. isSeeded: false,
  75. delegationDepth: 0,
  76. }),
  77. ...session.snapshotEvents().map(event => JSON.stringify({
  78. ...event,
  79. time: eventTimeOrigin + event.seq * 1_000,
  80. })),
  81. '',
  82. ].join('\n')
  83. }
  84. describe('web e2e: Markdown inline-code links', () => {
  85. let scaffold: WebScaffold
  86. let browser: Browser
  87. let page: Page
  88. let tripwire: ReturnType<typeof watchConsole>
  89. beforeAll(async () => {
  90. scaffold = await launchWebScaffold({})
  91. await seedSession(scaffold, markdownFixture(LINK_URL), SEED_ID)
  92. browser = await chromium.launch()
  93. page = await newEnglishPage(browser)
  94. await page.route('http://127.0.0.1:3199/**', async route => route.fulfill({
  95. contentType: 'text/html',
  96. body: '<h1>Inline-code preview</h1>',
  97. }))
  98. tripwire = watchConsole(page)
  99. await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
  100. await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
  101. }, 120_000)
  102. afterAll(async () => {
  103. await browser?.close()
  104. await scaffold?.close()
  105. })
  106. it.skipIf(MODE === 'record')('opens a complete HTTP URL from inline code and leaves other code inert', async () => {
  107. onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-inline-code-links'))
  108. const groupRow = page.locator('[role="treeitem"]').first()
  109. await groupRow.waitFor({ timeout: 15_000 })
  110. await groupRow.click()
  111. const sessionRow = page.locator('[role="treeitem"]').nth(1)
  112. await sessionRow.waitFor({ timeout: 10_000 })
  113. await sessionRow.click()
  114. await expect.poll(() => page.getByText(DONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
  115. const inlineCodeLink = page.locator('[class*="markdown"] code a')
  116. await expect.poll(() => inlineCodeLink.count(), { timeout: 10_000 }).toBe(1)
  117. expect(await inlineCodeLink.getAttribute('href')).toBe(LINK_URL)
  118. expect(await inlineCodeLink.getAttribute('target')).toBe('_blank')
  119. expect(await inlineCodeLink.getAttribute('rel')).toBe('noopener noreferrer')
  120. await inlineCodeLink.focus()
  121. expect(await inlineCodeLink.evaluate(element => document.activeElement === element)).toBe(true)
  122. await inlineCodeLink.click()
  123. const browserAddress = page.locator('[data-rightbar-col]')
  124. .getByRole('textbox', { name: 'Enter an HTTP(S) address' })
  125. await expect.poll(() => browserAddress.inputValue()).toBe(LINK_URL)
  126. expect(await page.getByText(`curl ${LINK_URL}`, { exact: true }).locator('a').count()).toBe(0)
  127. expect(await page.getByText('javascript:alert(1)', { exact: true }).locator('a').count()).toBe(0)
  128. const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
  129. .split(SEED_ID).join('{{seededId}}')
  130. .split(LINK_URL).join('{{linkUrl}}')
  131. await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
  132. expect(tripwire.pageErrors).toEqual([])
  133. expect(tripwire.warnings).toEqual([])
  134. await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
  135. }, 60_000)
  136. })