hermesMedia.spec.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { expect, test, type APIRequestContext, type Locator } from '@playwright/test'
  2. const mediaPaths = [
  3. '/Users/samien/.hermes/profiles/yaoer/cache/images/openai_codex_gpt-image-2-high_20260902_194820_1d225f08.png',
  4. '/Users/samien/.hermes/profiles/yaoer/cache/images/国漫三视图.png',
  5. ]
  6. const mediaNames = mediaPaths.map(path => path.split('/').at(-1)!)
  7. const mediaLabels = ['3D 半写实女性四视图', '国漫三视图']
  8. async function signIn(request: APIRequestContext, origin: string) {
  9. const bootstrap = await (await request.get('/api/app/bootstrap')).json()
  10. if (bootstrap.setupRequired) {
  11. const setup = await request.post('/api/app/setup', {
  12. headers: { Origin: origin, 'X-CSRF-Token': bootstrap.csrfToken },
  13. data: { username: 'admin', password: 'e2e-password' },
  14. })
  15. expect(setup.ok()).toBe(true)
  16. return
  17. }
  18. const login = await request.post('/api/app/login', {
  19. headers: { Origin: origin, 'X-CSRF-Token': bootstrap.csrfToken },
  20. data: { username: 'admin', password: 'e2e-password' },
  21. })
  22. expect(login.ok()).toBe(true)
  23. }
  24. async function expectDecodedImage(image: Locator) {
  25. await expect(image).toBeVisible()
  26. await expect.poll(() => image.evaluate(element => {
  27. const img = element instanceof HTMLImageElement ? element : element.querySelector('img')
  28. return img?.naturalWidth ?? 0
  29. })).toBeGreaterThan(0)
  30. }
  31. test('loads Hermes profile cache images through the Web server and switches their full-size previews', async ({ page, baseURL }, testInfo) => {
  32. await signIn(page.request, new URL(baseURL!).origin)
  33. // No image interception: this exercises the real Web route, upstream account
  34. // authentication, attachment-to-inline response handling, and browser decode.
  35. const responses = mediaPaths.map(path => page.waitForResponse(response => (
  36. new URL(response.url()).pathname === '/api/files/download' && new URL(response.url()).searchParams.get('path') === path
  37. )))
  38. await page.goto('/chat/session-media?profile=yaoer')
  39. await expect(page).toHaveTitle('瑶儿生成图片验收 · 夭夭 AI')
  40. for (const [index, name] of mediaLabels.entries()) {
  41. const image = page.getByRole('button', { name: `预览图片 ${name}`, exact: true })
  42. await image.scrollIntoViewIfNeeded()
  43. await expectDecodedImage(image)
  44. const response = await responses[index]!
  45. expect(response.status()).toBe(200)
  46. expect(response.headers()['content-type']).toContain('image/png')
  47. expect(response.headers()['content-disposition'] ?? '').not.toMatch(/^attachment(?:;|$)/)
  48. expect(new URL(response.url()).origin).toBe(new URL(baseURL!).origin)
  49. expect(new URL(response.url()).searchParams.get('profile')).toBe('yaoer')
  50. }
  51. await page.getByRole('button', { name: `预览图片 ${mediaLabels[0]}`, exact: true }).click()
  52. const preview = page.getByRole('dialog')
  53. await expect(preview).toHaveAccessibleName(`预览 ${mediaNames[0]}`)
  54. await expectDecodedImage(preview.getByRole('img', { name: mediaNames[0], exact: true }))
  55. await expect(preview.getByRole('button', { name: '下一张媒体' })).toBeVisible()
  56. await preview.getByRole('button', { name: '下一张媒体' }).click()
  57. await expect(preview).toHaveAccessibleName(`预览 ${mediaNames[1]}`)
  58. const secondPreview = preview.getByRole('img', { name: mediaNames[1], exact: true })
  59. await expectDecodedImage(secondPreview)
  60. expect(new URL(await secondPreview.getAttribute('src') || '', baseURL).searchParams.get('path')).toBe(mediaPaths[1])
  61. await expect(preview.getByRole('button', { name: '上一张媒体' })).toBeVisible()
  62. await expect(preview.getByRole('button', { name: '下一张媒体' })).toBeHidden()
  63. await page.keyboard.press('ArrowLeft')
  64. await expect(preview).toHaveAccessibleName(`预览 ${mediaNames[0]}`)
  65. await expectDecodedImage(preview.getByRole('img', { name: mediaNames[0], exact: true }))
  66. await expect(preview.getByRole('button', { name: '下一张媒体' })).toBeVisible()
  67. await page.screenshot({ path: testInfo.outputPath('hermes-profile-cache-preview.png') })
  68. await page.keyboard.press('Escape')
  69. await expect(preview).toBeHidden()
  70. })
  71. test('restores nine user Profile uploads as images and opens the whole batch in the preview', async ({ page, baseURL }, testInfo) => {
  72. await signIn(page.request, new URL(baseURL!).origin)
  73. await page.goto('/chat/session-user-media?profile=yaoer')
  74. const message = page.locator('.message--user')
  75. await expect(message).toContainText('读取图片中的提示词')
  76. await expect(message).not.toContainText('@image:')
  77. await expect(message).not.toContainText('[用户附加图片')
  78. await expect(message).not.toContainText('[screenshot]')
  79. const images = message.locator('img')
  80. await expect(images).toHaveCount(9)
  81. for (const image of await images.all()) {
  82. await image.scrollIntoViewIfNeeded()
  83. await expectDecodedImage(image)
  84. const url = new URL(await image.getAttribute('src') || '', baseURL)
  85. expect(url.origin).toBe(new URL(baseURL!).origin)
  86. expect(url.pathname).toBe('/api/files/download')
  87. expect(url.searchParams.get('path')).toContain('/.hermes/profiles/yaoer/images/')
  88. expect(url.searchParams.get('profile')).toBe('yaoer')
  89. }
  90. await page.screenshot({ path: testInfo.outputPath('user-profile-images.png') })
  91. await images.first().click()
  92. const preview = page.locator('.image-preview-overlay')
  93. await expect(preview.locator('.image-preview-counter')).toHaveText('1 / 9')
  94. await expectDecodedImage(preview.getByRole('img', { name: '照片-1.png', exact: true }))
  95. await preview.getByRole('button', { name: '下一张媒体' }).click()
  96. await expect(preview.locator('.image-preview-counter')).toHaveText('2 / 9')
  97. await expectDecodedImage(preview.getByRole('img', { name: '照片-2.png', exact: true }))
  98. await page.screenshot({ path: testInfo.outputPath('user-profile-image-preview.png') })
  99. })
  100. test('server permissions reject a previously readable image and retry succeeds after authorization', async ({page,baseURL}) => {
  101. const origin=new URL(baseURL!).origin
  102. await signIn(page.request,origin)
  103. const capabilities=await(await page.request.get('/api/realtime/capabilities')).json()
  104. const headers={Origin:origin,'X-CSRF-Token':capabilities.csrfToken}
  105. const policy=async(mode:'folders'|'all')=>{
  106. const response=await page.request.put('/api/app/settings/file-access',{headers,data:{mode,folders:[]}})
  107. expect(response.ok(),await response.text()).toBe(true)
  108. }
  109. await policy('folders')
  110. try {
  111. await page.goto('/chat/session-media?profile=yaoer')
  112. const failure=page.getByRole('button',{name:`${mediaLabels[0]} · 加载失败,点击重试`,exact:true})
  113. await expect(failure).toBeVisible()
  114. const denied=await page.request.get('/api/files/download',{params:{path:mediaPaths[0]!,profile:'yaoer',preview:'1'}})
  115. expect(denied.status()).toBe(403)
  116. await policy('all')
  117. await failure.click()
  118. await expectDecodedImage(page.getByRole('button',{name:`预览图片 ${mediaLabels[0]}`,exact:true}))
  119. await expect(failure).toHaveCount(0)
  120. } finally {await policy('all')}
  121. })