managedBrowserCards.test.ts 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  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.find('.managed-browser-card button').exists()).toBe(false)
  37. }finally{wrapper.unmount()}
  38. })
  39. it('updates a card from server events and preserves it across a fresh transcript snapshot',()=>{
  40. const conversation={id:'chat',kind:'direct',memberIds:['bot'],lastSeq:1} as any
  41. const store=new WorkspaceTranscriptStore()
  42. store.hydrate({agents:[],conversations:[conversation],details:[{conversation,messages:[message],cursor:1,hasOlder:false,run:null,interactions:[],context:null}],cursor:1})
  43. const changed={...message,browserCard:{...card,status:'preparing' as const,installation:{status:'installing' as const,message:'下载中 60%',progress:60,updatedAt:2},updatedAt:2}}
  44. store.apply({type:'message.changed',seq:2,conversationId:'chat',data:changed})
  45. expect(workspaceMessagesToUi(store.get('chat')!.messages)[0]?.browserCard).toEqual(changed.browserCard)
  46. const snapshot=JSON.parse(JSON.stringify(store.get('chat')))
  47. const refreshed=new WorkspaceTranscriptStore()
  48. refreshed.hydrate({agents:[],conversations:[conversation],details:[snapshot],cursor:2})
  49. expect(workspaceMessagesToUi(refreshed.get('chat')!.messages)[0]?.browserCard?.installation?.progress).toBe(60)
  50. })
  51. it('shows real installation progress, errors and a keyboard-accessible retry',async()=>{
  52. const wrapper=mount(Card,{props:{card:{...card,status:'preparing',installation:{status:'installing',message:'下载中 45%',progress:45,updatedAt:1}}},global:{stubs:{AppIcon:true}}})
  53. try{
  54. expect(wrapper.get('progress').attributes('value')).toBe('45')
  55. expect(wrapper.get('[role="status"]').text()).toBe('下载中 45%')
  56. await wrapper.setProps({card:{...card,status:'failed',installation:{status:'failed',message:'安装失败',error:'空间不足',updatedAt:2}}})
  57. expect(wrapper.get('[role="alert"]').text()).toBe('空间不足')
  58. expect(wrapper.get('button').text()).toBe('重试并接管浏览器')
  59. await wrapper.setProps({readOnly:true})
  60. expect(wrapper.find('button').exists()).toBe(false)
  61. }finally{wrapper.unmount()}
  62. })