composer.test.ts 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import { mount } from '@vue/test-utils'
  2. import { describe, expect, it } from 'vitest'
  3. import ComposerShell from '@/components/composer/ComposerShell.vue'
  4. describe('composer availability', () => {
  5. it('keeps the group-chat send action available while an Agent is running', () => {
  6. const wrapper = mount(ComposerShell, { props: { mode: 'group', streaming: true } })
  7. expect(wrapper.find('[aria-label="发送消息"]').exists()).toBe(true)
  8. expect(wrapper.find('[aria-label="停止生成"]').exists()).toBe(false)
  9. })
  10. it('keeps the ordinary-chat interrupt action while its server run is active', () => {
  11. const wrapper = mount(ComposerShell, { props: { mode: 'chat', streaming: true } })
  12. expect(wrapper.find('[aria-label="停止生成"]').exists()).toBe(true)
  13. expect(wrapper.find('[aria-label="发送消息"]').exists()).toBe(false)
  14. })
  15. it('places an accessible blue fast-mode toggle beside the model control', () => {
  16. const wrapper = mount(ComposerShell, { props: { mode: 'chat', modelLabel: 'gpt-5.6-terra', fastMode: true } })
  17. const model = wrapper.get('.composer-tool--model')
  18. const fast = wrapper.get('.composer-fast-mode')
  19. expect(model.element.nextElementSibling).toBe(fast.element)
  20. expect(fast.attributes('aria-pressed')).toBe('true')
  21. expect(fast.classes()).toContain('active')
  22. fast.trigger('click')
  23. expect(wrapper.emitted('fastModeToggle')?.[0]).toEqual([false])
  24. })
  25. it('keeps named group activity directly above the input after a reference', () => {
  26. const wrapper = mount(ComposerShell, {
  27. props: {
  28. mode: 'group',
  29. activityText: '夭夭正在输入…',
  30. reference: { id: 'message-1', author: '夭夭', content: '上一条消息' },
  31. },
  32. })
  33. const reference = wrapper.get('.composer-reference')
  34. const activity = wrapper.get('.composer-activity-slot')
  35. const shell = wrapper.get('.composer-shell')
  36. expect(reference.element.nextElementSibling).toBe(activity.element)
  37. expect(activity.element.nextElementSibling).toBe(shell.element)
  38. expect(activity.get('[role="status"]').text()).toBe('夭夭正在输入…')
  39. })
  40. it('adds attachments when randomUUID is unavailable in an HTTP browser', async () => {
  41. const descriptor = Object.getOwnPropertyDescriptor(globalThis.crypto, 'randomUUID')
  42. Object.defineProperty(globalThis.crypto, 'randomUUID', { configurable: true, value: undefined })
  43. try {
  44. const wrapper = mount(ComposerShell)
  45. const input = wrapper.get<HTMLInputElement>('.composer-file-input')
  46. const file = new File(['兼容内容'], '兼容.txt', { type: 'text/plain', lastModified: 123 })
  47. Object.defineProperty(input.element, 'files', { configurable: true, value: [file] })
  48. await input.trigger('change')
  49. expect(wrapper.get('.composer-attachment strong').text()).toBe('兼容.txt')
  50. expect(wrapper.find('[role="alert"]').exists()).toBe(false)
  51. wrapper.unmount()
  52. } finally {
  53. if (descriptor) Object.defineProperty(globalThis.crypto, 'randomUUID', descriptor)
  54. else Reflect.deleteProperty(globalThis.crypto, 'randomUUID')
  55. }
  56. })
  57. })
  58. it('keeps a newer draft even when its text is edited back to the submitted value', async () => {
  59. const wrapper = mount(ComposerShell, { props: { mode: 'group', draftKey: 'receipt-draft' } })
  60. const input = wrapper.get('textarea')
  61. await input.setValue('原消息')
  62. const api = wrapper.vm as unknown as { submissionToken(): { key: string; revision: number }; clearAfterSend(token?: { key: string; revision: number }): boolean }
  63. const token = api.submissionToken()
  64. await input.setValue('新草稿'); await input.setValue('原消息')
  65. expect(api.clearAfterSend(token)).toBe(false)
  66. expect((input.element as HTMLTextAreaElement).value).toBe('原消息')
  67. api.clearAfterSend()
  68. await wrapper.vm.$nextTick()
  69. expect((input.element as HTMLTextAreaElement).value).toBe('')
  70. wrapper.unmount()
  71. })
  72. it('clears only the submitted stored draft after switching conversations', async () => {
  73. const wrapper = mount(ComposerShell, { props: { mode: 'group', draftKey: 'receipt-a' } })
  74. await wrapper.get('textarea').setValue('已发送 A')
  75. const api = wrapper.vm as unknown as { submissionToken(): { key: string; revision: number }; clearAfterSend(token: { key: string; revision: number }): boolean }
  76. const token = api.submissionToken()
  77. await wrapper.setProps({ draftKey: 'receipt-b' }); await wrapper.get('textarea').setValue('保留 B')
  78. expect(api.clearAfterSend(token)).toBe(false)
  79. expect((wrapper.get('textarea').element as HTMLTextAreaElement).value).toBe('保留 B')
  80. expect(localStorage.getItem(token.key)).toBeNull()
  81. wrapper.unmount()
  82. })