managedBrowserCards.test.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import { afterEach, expect, it, vi } from 'vitest'
  2. import { flushPromises, mount } from '@vue/test-utils'
  3. import { createPinia } from 'pinia'
  4. import Card from '@/components/messages/ManagedBrowserCard.vue'
  5. import Timeline from '@/components/workspace/WorkspaceMessageTimeline.vue'
  6. import { workspaceMessagesToUi } from '@/components/workspace/viewModels'
  7. import { WorkspaceTranscriptStore } from '@/components/workspace/transcriptStore'
  8. import { buildMessageTimelineRows } from '@/utils/turnTrace'
  9. import type { ManagedBrowserCard } from '@shared/managedBrowser'
  10. import type { WorkspaceMessage } from '@shared/workspace'
  11. const card:ManagedBrowserCard={id:'browser-session',agentId:'bot',agentName:'竹儿',status:'active',title:'真实网页标题',url:'https://example.com/path',updatedAt:1}
  12. const message:WorkspaceMessage={id:'browser-message',conversationId:'chat',agentId:'bot',agentName:'竹儿',role:'assistant',content:'',reasoning:'',status:'complete',seq:1,createdAt:1,tools:[{id:'tool',name:'managed_browser_open',status:'tool.complete'}],attachments:[],browserCard:card}
  13. afterEach(()=>{vi.restoreAllMocks();vi.unstubAllGlobals()})
  14. it('preserves a structured browser card through history serialization and trace aggregation',()=>{
  15. const restored=JSON.parse(JSON.stringify(message)) as WorkspaceMessage
  16. const [ui]=workspaceMessagesToUi([restored])
  17. expect(ui?.browserCard).toEqual(card)
  18. const rows=buildMessageTimelineRows([ui!])
  19. expect(rows.map(row=>row.kind)).toEqual(['trace','message'])
  20. expect(rows[1]?.kind==='message'&&rows[1].message.browserCard).toEqual(card)
  21. expect(workspaceMessagesToUi([{...message,browserCard:undefined,content:JSON.stringify({browserCard:card})}])[0]?.browserCard).toBeUndefined()
  22. })
  23. it('renders browser cards with tools hidden and emits only their structured control target',async()=>{
  24. vi.stubGlobal('ResizeObserver',class {observe(){}disconnect(){}})
  25. const wrapper=mount(Timeline,{props:{identity:'chat',messages:workspaceMessagesToUi([message]),showTools:false},global:{plugins:[createPinia()],stubs:{AppIcon:true,AgentAvatar:true,MarkdownContent:true}}})
  26. try{
  27. await flushPromises()
  28. expect(wrapper.text()).toContain('真实网页标题')
  29. expect(wrapper.text()).toContain('https://example.com/path')
  30. expect(wrapper.find('.turn-trace').exists()).toBe(false)
  31. await wrapper.get('.managed-browser-card button').trigger('click')
  32. expect(wrapper.emitted('browserControl')).toEqual([[card]])
  33. const finished={...message,browserCard:{...card,status:'closed' as const,updatedAt:2}}
  34. await wrapper.setProps({messages:workspaceMessagesToUi([finished])})
  35. expect(wrapper.text()).toContain('会话已结束')
  36. expect(wrapper.get('.managed-browser-card button').text()).toBe('打开此 Bot 的浏览器')
  37. expect(wrapper.text()).toContain('已关闭或回收的页面不会自动恢复')
  38. await wrapper.get('.managed-browser-card button').trigger('click')
  39. expect(wrapper.emitted('browserControl')?.at(-1)).toEqual([finished.browserCard])
  40. }finally{wrapper.unmount()}
  41. })
  42. it('keeps idle browser cards actionable across tasks and respects read-only history',async()=>{
  43. const idle={...card,status:'idle' as const}
  44. const wrapper=mount(Card,{props:{card:idle},global:{stubs:{AppIcon:true}}})
  45. try{
  46. expect(wrapper.text()).toContain('页面已保留')
  47. expect(wrapper.text()).toContain('页面可跨任务继续使用')
  48. expect(wrapper.text()).toContain('真实网页标题')
  49. expect(wrapper.text()).toContain('https://example.com/path')
  50. await wrapper.get('button').trigger('click')
  51. expect(wrapper.emitted('open')).toEqual([[idle]])
  52. await wrapper.setProps({readOnly:true})
  53. expect(wrapper.find('button').exists()).toBe(false)
  54. await wrapper.setProps({card:{...card,status:'closed'}})
  55. expect(wrapper.find('button').exists()).toBe(false)
  56. }finally{wrapper.unmount()}
  57. })
  58. it('updates a card from server events and preserves it across a fresh transcript snapshot',()=>{
  59. const conversation={id:'chat',kind:'direct',memberIds:['bot'],lastSeq:1} as any
  60. const store=new WorkspaceTranscriptStore()
  61. store.hydrate({agents:[],conversations:[conversation],details:[{conversation,messages:[message],cursor:1,hasOlder:false,run:null,interactions:[],context:null}],cursor:1})
  62. const changed={...message,browserCard:{...card,status:'preparing' as const,installation:{status:'installing' as const,message:'下载中 60%',progress:60,updatedAt:2},updatedAt:2}}
  63. store.apply({type:'message.changed',seq:2,conversationId:'chat',data:changed})
  64. expect(workspaceMessagesToUi(store.get('chat')!.messages)[0]?.browserCard).toEqual(changed.browserCard)
  65. const snapshot=JSON.parse(JSON.stringify(store.get('chat')))
  66. const refreshed=new WorkspaceTranscriptStore()
  67. refreshed.hydrate({agents:[],conversations:[conversation],details:[snapshot],cursor:2})
  68. expect(workspaceMessagesToUi(refreshed.get('chat')!.messages)[0]?.browserCard?.installation?.progress).toBe(60)
  69. })
  70. it('shows real installation progress, errors and a keyboard-accessible retry',async()=>{
  71. const wrapper=mount(Card,{props:{card:{...card,status:'preparing',installation:{status:'installing',message:'下载中 45%',progress:45,updatedAt:1}}},global:{stubs:{AppIcon:true}}})
  72. try{
  73. expect(wrapper.get('progress').attributes('value')).toBe('45')
  74. expect(wrapper.get('[role="status"]').text()).toBe('下载中 45%')
  75. await wrapper.setProps({card:{...card,status:'failed',installation:{status:'failed',message:'安装失败',error:'空间不足',updatedAt:2}}})
  76. expect(wrapper.get('[role="alert"]').text()).toBe('空间不足')
  77. expect(wrapper.get('button').text()).toBe('重试并接管浏览器')
  78. await wrapper.setProps({readOnly:true})
  79. expect(wrapper.find('button').exists()).toBe(false)
  80. }finally{wrapper.unmount()}
  81. })