submission-echo.e2e.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. // @vitest-environment jsdom
  2. // Local submission echo over the BUILT client graph (keyless fixture Connection RPC
  3. // transport): a text-plus-image send paints its echo bubble synchronously on
  4. // the submit keystroke — before serialization, transport, or the fixture's
  5. // durable admission — with the composer already cleared and editable, and the
  6. // durable user/message replaces the echo without a duplicate. The fixture host
  7. // echoes the prompt requestId as the durable source's rpcId, so the retirement
  8. // path here is the production correlation, not a test hook.
  9. import { fireEvent, screen, waitFor } from '@testing-library/react'
  10. import { expect, it } from 'vitest'
  11. import { installAssembledBootEnv, mountAssembledApp } from './assembled-boot.ts'
  12. installAssembledBootEnv()
  13. it('paints the submission echo on the send keystroke and swaps it for the durable node', async () => {
  14. mountAssembledApp()
  15. const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
  16. const start = tree.querySelector<HTMLButtonElement>('button[aria-label="New session in fixture"]')
  17. if (start === null) throw new Error('fixture Workspace new-session action missing')
  18. fireEvent.click(start)
  19. const composer = await waitFor(() => {
  20. const surface = document.querySelector<HTMLElement>('[data-composer-input]')
  21. if (surface === null) throw new Error('composer surface missing')
  22. return surface
  23. }, { timeout: 10_000 })
  24. const image = new File([new Uint8Array([137, 80, 78, 71])], 'echoed.png', { type: 'image/png' })
  25. fireEvent.paste(composer, {
  26. clipboardData: {
  27. items: [{ kind: 'file', type: 'image/png', getAsFile: () => image }],
  28. getData: () => '',
  29. },
  30. })
  31. await waitFor(() => {
  32. if (document.querySelector('[role="group"][aria-label="Pending attachments"] img') === null) {
  33. throw new Error('attachment rail missing')
  34. }
  35. }, { timeout: 5_000 })
  36. fireEvent.paste(composer, {
  37. clipboardData: { items: [], getData: () => '回显这条消息' },
  38. })
  39. await waitFor(() => { expect(composer.textContent).toBe('回显这条消息') })
  40. fireEvent.keyDown(composer, { key: 'Enter' })
  41. // Synchronously after the keystroke: the echo bubble is in the flow with
  42. // the draft text and the object-URL preview, while the prompt has not even
  43. // been serialized yet (it starts after a paint yield). The composer is
  44. // already cleared, editable, and free of the rail.
  45. const echo = document.querySelector<HTMLElement>('[data-submission-echo]')
  46. if (echo === null) throw new Error('submission echo missing on the send keystroke')
  47. expect(echo.textContent).toContain('回显这条消息')
  48. expect(echo.querySelector('img')?.getAttribute('src')?.split(':')[0]).toBe('blob')
  49. expect(composer.textContent).toBe('')
  50. expect(composer.getAttribute('contenteditable')).toBe('true')
  51. expect(document.querySelector('[role="group"][aria-label="Pending attachments"]')).toBeNull()
  52. // The fixture's durable user/message (source.rpcId echoes the prompt
  53. // requestId) replaces the echo: one bubble, no marker left, and the image
  54. // now renders from the durable gallery.
  55. await waitFor(() => {
  56. if (document.querySelector('[data-submission-echo]') !== null) {
  57. throw new Error('submission echo still present after the durable node arrived')
  58. }
  59. }, { timeout: 10_000 })
  60. expect(screen.getAllByText('回显这条消息')).toHaveLength(1)
  61. await waitFor(() => {
  62. if (document.querySelector('[data-align="end"] img') === null) {
  63. throw new Error('durable user gallery missing')
  64. }
  65. }, { timeout: 10_000 })
  66. })