trajectory-image-display.expected.e2e.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. // @vitest-environment jsdom
  2. // Trajectory image surfaces over the BUILT client graph (the code-mode-fixture
  3. // idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
  4. // Opens the fixture history session whose turn 73 carries an image in BOTH a
  5. // user message and an assistant message, and pins the Trajectory surfaces:
  6. // selecting the ledger record renders the shared ui-attachment gallery from
  7. // the durable session-log reference, and the browser URL is the SAME object
  8. // URL Chat resolved — one sessions.attachment read per session attachment.
  9. import { fireEvent, screen, waitFor, within } from '@testing-library/react'
  10. import { expect, it, vi } from 'vitest'
  11. import { installAssembledBootEnv, mountAssembledApp } from './assembled-boot.ts'
  12. installAssembledBootEnv()
  13. /** Open the fixture history session and wait for the Chat gallery to load. */
  14. async function openFixtureSession(): Promise<void> {
  15. const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
  16. const group = (await within(tree).findAllByText('fixture'))
  17. .map(el => el.closest<HTMLElement>('[role="treeitem"]'))
  18. .find(el => el?.getAttribute('aria-expanded') !== null)
  19. if (group === null || group === undefined) throw new Error('fixture Workspace group missing')
  20. if (group.getAttribute('aria-expanded') === 'false') {
  21. fireEvent.click(within(group).getByText('fixture'))
  22. await waitFor(() => {
  23. expect(group.getAttribute('aria-expanded')).toBe('true')
  24. })
  25. }
  26. const session = await within(tree).findByText('Fixture 历史会话')
  27. fireEvent.click(session)
  28. await waitFor(() => {
  29. expect(document.querySelectorAll('[data-align] img').length).toBeGreaterThan(0)
  30. }, { timeout: 10_000 })
  31. }
  32. /** Scroll the virtual ledger until the row whose text contains `needle` mounts. */
  33. async function scrollRowIntoWindow(needle: string): Promise<HTMLElement> {
  34. await waitFor(() => {
  35. if (document.querySelectorAll('tr[data-trajectory-row-key]').length === 0) {
  36. throw new Error('trajectory rows not mounted')
  37. }
  38. }, { timeout: 10_000 })
  39. const pane = document.querySelector('[data-trajectory-scroll] table')?.parentElement
  40. if (!(pane instanceof HTMLElement)) throw new Error('trajectory scroll pane missing')
  41. for (let top = 0; top <= 40_000; top += 1_000) {
  42. pane.scrollTop = top
  43. fireEvent.scroll(pane)
  44. // Let the virtualizer publish the new window before probing.
  45. await new Promise(resolve => setTimeout(resolve, 25))
  46. const hit = [...document.querySelectorAll<HTMLElement>('tr[data-trajectory-row-key]')]
  47. .find(row => row.textContent?.includes(needle))
  48. if (hit !== undefined) return hit
  49. }
  50. throw new Error(`trajectory row containing ${JSON.stringify(needle)} never mounted`)
  51. }
  52. it('renders durable record images in the Trajectory details panel from the shared cache', async () => {
  53. // The virtual ledger needs a measurable viewport; jsdom reports zero
  54. // heights, so pin one and neutralize the imperative tail scroll.
  55. vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600)
  56. Object.defineProperty(HTMLElement.prototype, 'scrollTo', {
  57. configurable: true,
  58. value: () => {},
  59. })
  60. mountAssembledApp()
  61. await openFixtureSession()
  62. const chatSrc = document.querySelector('[data-align="end"] img')?.getAttribute('src')
  63. if (chatSrc === null || chatSrc === undefined) throw new Error('chat gallery image missing')
  64. fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
  65. const userRow = await scrollRowIntoWindow('历史用户图片')
  66. fireEvent.click(userRow)
  67. // Selecting the record opens the details panel; the ui-attachment gallery
  68. // resolves the durable reference through the SAME per-session cache Chat
  69. // used, so the object URL is identical — no second attachment read.
  70. const panel = await screen.findByRole('tabpanel')
  71. await waitFor(() => {
  72. expect(within(panel).getAllByRole('img').length).toBeGreaterThan(0)
  73. }, { timeout: 10_000 })
  74. expect(within(panel).getAllByRole('img').map(img => ({
  75. alt: img.getAttribute('alt'),
  76. scheme: img.getAttribute('src')?.split(':')[0],
  77. sharedWithChat: img.getAttribute('src') === chatSrc,
  78. }))).toMatchInlineSnapshot(`
  79. [
  80. {
  81. "alt": "fixture-image.png",
  82. "scheme": "blob",
  83. "sharedWithChat": true,
  84. },
  85. ]
  86. `)
  87. })