markdown-images.e2e.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. // Web e2e scenario: absolute HTTP(S) Markdown images. A validated session
  2. // assembled through the Session API is seeded cold into the real web
  3. // composition, then a separate image origin proves that the browser receives
  4. // a real network image while local-path Markdown remains inert alt text.
  5. import { createServer, type Server } from 'node:http'
  6. import { fileURLToPath } from 'node:url'
  7. import type { Browser, Page } from 'playwright'
  8. import { chromium } from 'playwright'
  9. import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
  10. import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
  11. import {
  12. SESSION_FORMAT_VERSION,
  13. Session,
  14. SessionId,
  15. } from '@deepseek-ai/dsh-session'
  16. import type {} from '@deepseek-ai/dsh-session-title'
  17. import {
  18. assertFixtureInventory,
  19. captureStableAria,
  20. compareOrRefreshGolden,
  21. launchWebScaffold,
  22. seedSession,
  23. watchConsole,
  24. webSnapshotMode,
  25. type WebScaffold,
  26. } from './scaffold.ts'
  27. import { newEnglishPage, saveFailureShot } from './support.ts'
  28. const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/markdown-images', import.meta.url))
  29. const UI_EXPECTED = fileURLToPath(new URL('./snapshots/markdown-images/ui.expected.md', import.meta.url))
  30. const MODE = webSnapshotMode()
  31. const SEED_ID = 'markdown-images-web-e2e'
  32. const REMOTE_ALT = 'Remote test image'
  33. const LOCAL_ALT = 'Local test image'
  34. const PNG = Buffer.from(
  35. 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
  36. 'base64',
  37. )
  38. interface ImageOrigin {
  39. server: Server
  40. url: string
  41. requests: Array<{ path: string | undefined; referer: string | undefined }>
  42. }
  43. /** Start the deterministic remote image origin used by this browser scenario. */
  44. async function startImageOrigin(): Promise<ImageOrigin> {
  45. const requests: ImageOrigin['requests'] = []
  46. const server = createServer((request, response) => {
  47. requests.push({ path: request.url, referer: request.headers.referer })
  48. response.writeHead(200, {
  49. 'cache-control': 'no-store',
  50. 'content-length': PNG.length,
  51. 'content-type': 'image/png',
  52. })
  53. response.end(PNG)
  54. })
  55. await new Promise<void>((resolve, reject) => {
  56. server.once('error', reject)
  57. server.listen(0, '127.0.0.1', resolve)
  58. })
  59. const address = server.address()
  60. if (address === null || typeof address === 'string') {
  61. throw new Error('image origin did not expose an IP socket')
  62. }
  63. return {
  64. server,
  65. url: `http://127.0.0.1:${String(address.port)}/image.png`,
  66. requests,
  67. }
  68. }
  69. /** Stop one image origin after the browser and host release their requests. */
  70. async function stopServer(server: Server): Promise<void> {
  71. await new Promise<void>((resolve, reject) => {
  72. server.close((error) => {
  73. if (error === undefined) resolve()
  74. else reject(error)
  75. })
  76. })
  77. }
  78. /** Build one closed, invariant-checked session fixture with remote and local image Markdown. */
  79. function markdownImageFixture(remoteUrl: string): string {
  80. const session = Session.create(SessionId('markdown-image-source'))
  81. session.append('turn/start', {
  82. turn: 1,
  83. trigger: { kind: 'message', source: { kind: 'user' } },
  84. })
  85. const user = session.append('user/message', createUserMessage({
  86. content: [{ type: 'text', text: 'Show the Markdown image policy.' }],
  87. source: { kind: 'user' },
  88. }), { surfaceOp: 'append' })
  89. session.append('session/title', {
  90. title: 'Markdown image policy',
  91. messageSeqs: [user.seq],
  92. source: { kind: 'fallback' },
  93. })
  94. session.append('step/start', { turn: 1, step: 1 })
  95. session.append('assistant/message', {
  96. turn: 1,
  97. step: 1,
  98. message: createMessage({
  99. role: 'assistant',
  100. content: [{
  101. type: 'text',
  102. text: [
  103. '## Markdown images',
  104. '',
  105. `![${REMOTE_ALT}](${remoteUrl})`,
  106. '',
  107. `![${LOCAL_ALT}](./local-image.png)`,
  108. '',
  109. 'REMOTE_IMAGE_DONE',
  110. ].join('\n'),
  111. }],
  112. source: { kind: 'model', provider: 'fixture', model: 'fixture' },
  113. }),
  114. }, { surfaceOp: 'append' })
  115. session.append('step/end', { turn: 1, step: 1 })
  116. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  117. const header = {
  118. type: 'session',
  119. version: SESSION_FORMAT_VERSION,
  120. id: '{{sessionId}}',
  121. createdAt: 0,
  122. cwd: '{{cwd}}',
  123. }
  124. return [
  125. JSON.stringify(header),
  126. ...session.events.map(event => JSON.stringify(event)),
  127. '',
  128. ].join('\n')
  129. }
  130. describe('web e2e: remote Markdown image rendering', () => {
  131. let scaffold: WebScaffold
  132. let imageOrigin: ImageOrigin
  133. let browser: Browser
  134. let page: Page
  135. let tripwire: ReturnType<typeof watchConsole>
  136. beforeAll(async () => {
  137. imageOrigin = await startImageOrigin()
  138. scaffold = await launchWebScaffold({})
  139. await seedSession(scaffold, markdownImageFixture(imageOrigin.url), SEED_ID)
  140. browser = await chromium.launch()
  141. page = await newEnglishPage(browser)
  142. tripwire = watchConsole(page)
  143. await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
  144. await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
  145. }, 120_000)
  146. afterAll(async () => {
  147. await browser?.close()
  148. await scaffold?.close()
  149. await stopServer(imageOrigin.server)
  150. })
  151. it.skipIf(MODE === 'record')('loads only the remote image and matches the conversation golden', async () => {
  152. onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-images'))
  153. const groupRow = page.locator('[role="treeitem"]').first()
  154. await groupRow.waitFor({ timeout: 15_000 })
  155. await groupRow.click()
  156. const sessionRow = page.locator('[role="treeitem"]').nth(1)
  157. await sessionRow.waitFor({ timeout: 10_000 })
  158. await sessionRow.click()
  159. await expect.poll(() => page.getByText('REMOTE_IMAGE_DONE', { exact: true }).count(), {
  160. timeout: 15_000,
  161. }).toBe(1)
  162. const image = page.getByRole('img', { name: REMOTE_ALT })
  163. await image.waitFor({ timeout: 10_000 })
  164. await expect.poll(() => image.evaluate(element => (element as HTMLImageElement).naturalWidth), {
  165. timeout: 10_000,
  166. }).toBeGreaterThan(0)
  167. expect(await image.evaluate((element) => {
  168. const computed = getComputedStyle(element)
  169. return {
  170. borderRadius: computed.borderRadius,
  171. decoding: element.getAttribute('decoding'),
  172. loading: element.getAttribute('loading'),
  173. maxWidth: computed.maxWidth,
  174. referrerPolicy: element.getAttribute('referrerpolicy'),
  175. }
  176. })).toEqual({
  177. borderRadius: '8px',
  178. decoding: 'async',
  179. loading: 'lazy',
  180. maxWidth: '100%',
  181. referrerPolicy: 'no-referrer',
  182. })
  183. expect(await page.getByRole('img', { name: LOCAL_ALT }).count()).toBe(0)
  184. expect(await page.getByText(LOCAL_ALT, { exact: true }).count()).toBe(1)
  185. expect(imageOrigin.requests).toEqual([{ path: '/image.png', referer: undefined }])
  186. const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
  187. .split(SEED_ID).join('{{seededId}}')
  188. await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
  189. expect(tripwire.pageErrors).toEqual([])
  190. expect(tripwire.warnings).toEqual([])
  191. await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
  192. }, 60_000)
  193. })