managedBrowser.test.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import {afterEach,beforeEach,expect,it,vi} from 'vitest'
  2. import {flushPromises,mount} from '@vue/test-utils'
  3. import ComputerPanel from '@/components/workspace/ComputerPanel.vue'
  4. import Preview from '@/components/workspace/LocalVmChatPanel.vue'
  5. import Viewer from '@/views/ComputerViewerView.vue'
  6. import BrowserToolbar from '@/components/workspace/BrowserToolbar.vue'
  7. import {apiRequest} from '@/api/client'
  8. import type {ManagedBrowserState} from '@shared/managedBrowser'
  9. vi.mock('@/api/client',()=>({apiRequest:vi.fn()}))
  10. const route={params:{agentId:'bot'},query:{} as Record<string,unknown>}
  11. vi.mock('vue-router',()=>({useRoute:()=>route}))
  12. const request=vi.mocked(apiRequest)
  13. const agent={id:'bot',name:'竹儿',profile:'default',nodeId:'local'} as any
  14. const permission={supported:true,authorized:true,screen:true,accessibility:true,ready:true,fullAuthorized:true}
  15. const stubs={AppIcon:true,GrokAuthPanel:true}
  16. let enabled:boolean|undefined,state:ManagedBrowserState,controlled:boolean
  17. beforeEach(()=>{
  18. vi.spyOn(document,'hidden','get').mockReturnValue(false)
  19. route.query={};enabled=true;controlled=false
  20. state={enabled:true,available:true,open:false,generation:4,profile:'temporary',tabs:[],downloads:[]}
  21. request.mockReset()
  22. request.mockImplementation(async(path,options)=>{
  23. const url=new URL(path,'http://fixture'),backend=url.searchParams.get('backend')
  24. if(url.pathname==='/api/app/agents')return {agents:[agent]} as any
  25. if(url.pathname.endsWith('/host-tools'))return {scriptMachine:true,serverComputer:true,cloud:true,vm:true,managedBrowser:enabled} as any
  26. if(url.pathname.endsWith('/desktop-environment'))return {hosts:[{id:'local',name:'服务器',online:true,local:permission}]} as any
  27. if(url.pathname.endsWith('/cloud-computer'))return {configured:true,running:true,connected:true} as any
  28. if(url.pathname.endsWith('/local-vm'))return {enabled:true,image:true,container:'running',ready:true} as any
  29. if(url.pathname.endsWith('/managed-browser'))return structuredClone(state) as any
  30. if(url.pathname.endsWith('/managed-browser/action'))return {ok:true} as any
  31. if(url.pathname.endsWith('/frame'))return {id:'frame',data:backend,width:100,height:80,generation:4} as any
  32. if(url.pathname.includes('/computer')){
  33. if(options?.method==='POST'&&url.pathname.endsWith('/take')){
  34. controlled=true;state.open=true;state.tabs=[{id:'tab',title:'示例页面',url:'https://example.com',active:true}]
  35. }
  36. if(options?.method==='POST'&&url.pathname.endsWith('/giveback'))controlled=false
  37. return {mode:controlled?'human':backend==='managed-browser'&&!state.open?'off':'idle',backend:backend==='managed-browser'?'managed-browser':'local',hostName:'托管浏览器',generation:4,controlId:controlled?'control':undefined,token:'fixture-token'} as any
  38. }
  39. throw new Error(`Unexpected request ${path}`)
  40. })
  41. })
  42. afterEach(()=>{vi.restoreAllMocks();vi.clearAllMocks()})
  43. it.each([false,undefined])('keeps managed browsing absent with an off or older settings response (%s)',async(value)=>{
  44. enabled=value
  45. const wrapper=mount(ComputerPanel,{props:{agents:[agent],standalone:true},global:{stubs}})
  46. try{
  47. await flushPromises()
  48. expect(wrapper.find('option[value="managed-browser"]').exists()).toBe(false)
  49. expect(request.mock.calls.some(([path])=>path.includes('/managed-browser'))).toBe(false)
  50. expect(request.mock.calls.some(([,options])=>options?.method==='POST')).toBe(false)
  51. }finally{wrapper.unmount()}
  52. })
  53. it('shows the independent browser beside existing environments without opening it during preview',async()=>{
  54. const wrapper=mount(Preview,{props:{agents:[agent],isAdmin:true,active:true},global:{stubs}})
  55. try{
  56. await flushPromises()
  57. for(const key of ['desktop:local','cloud','vm','managed-browser'])expect(wrapper.find(`option[value="${key}"]`).exists()).toBe(true)
  58. await wrapper.get('[aria-label="显示的桌面"]').setValue('managed-browser');await flushPromises()
  59. expect(wrapper.get('.preview').text()).toContain('浏览器尚未打开')
  60. expect(wrapper.find('.preview img').exists()).toBe(false)
  61. expect(request.mock.calls.some(([path,options])=>path.includes('managed-browser')&&options?.method==='POST')).toBe(false)
  62. const section=wrapper.get('[aria-label="托管浏览器"]')
  63. await section.get('button').trigger('click')
  64. expect(wrapper.emitted('desktop')?.at(-1)).toEqual([agent,'managed-browser',undefined])
  65. expect(request.mock.calls.some(([path])=>path.includes('/computer/take'))).toBe(false)
  66. }finally{wrapper.unmount()}
  67. })
  68. it('requires takeover before navigation and routes browser actions and Linux shortcuts to the managed target',async()=>{
  69. const wrapper=mount(ComputerPanel,{props:{agents:[agent],backend:'managed-browser',standalone:true},global:{stubs}})
  70. try{
  71. await flushPromises()
  72. expect(wrapper.text()).toContain('浏览器尚未打开')
  73. expect(request.mock.calls.some(([,options])=>options?.method==='POST')).toBe(false)
  74. expect(wrapper.get('[aria-label="浏览器地址"]').attributes('disabled')).toBeDefined()
  75. await wrapper.findAll('button').find(button=>button.text()==='打开并控制')!.trigger('click');await flushPromises()
  76. expect(wrapper.get('.computer-screen img').attributes('src')).toContain('base64,managed-browser')
  77. expect(request.mock.calls.find(([path])=>path.includes('/computer/take'))?.[0]).toContain('backend=managed-browser')
  78. await wrapper.get('[aria-label="浏览器地址"]').setValue('https://example.org/')
  79. await wrapper.get('.browser-toolbar form').trigger('submit');await flushPromises()
  80. expect(request).toHaveBeenCalledWith('/api/app/agents/bot/managed-browser/action',{method:'POST',body:expect.objectContaining({controlId:'control',token:'fixture-token',requestId:expect.any(String),generation:4,action:{kind:'navigate',url:'https://example.org/'}})})
  81. expect(request.mock.calls.some(([path])=>path.includes('/computer/browser')||path.endsWith('/browser'))).toBe(false)
  82. const action=request.mock.calls.find(([path])=>path.endsWith('/managed-browser/action'))?.[1]?.body
  83. expect(action).not.toHaveProperty('frameId')
  84. await wrapper.findAll('button').find(button=>button.text()==='键盘与操作')!.trigger('click')
  85. await wrapper.findAll('button').find(button=>button.text()==='全选')!.trigger('click');await flushPromises()
  86. expect(request).toHaveBeenCalledWith('/api/app/agents/bot/computer/input?backend=managed-browser',{method:'POST',body:expect.objectContaining({generation:4,frameId:'frame',action:{kind:'key',key:'a',modifiers:['ctrl']}})})
  87. await wrapper.get('.computer-screen img').trigger('keydown',{key:'c',metaKey:true});await flushPromises()
  88. expect(request).toHaveBeenCalledWith('/api/app/agents/bot/computer/input?backend=managed-browser',{method:'POST',body:expect.objectContaining({action:{kind:'key',key:'c',modifiers:['ctrl']}})})
  89. await wrapper.findAll('button').find(button=>button.text()==='结束控制')!.trigger('click');await flushPromises()
  90. expect(request.mock.calls.find(([path])=>path.includes('/giveback'))?.[0]).toContain('backend=managed-browser')
  91. }finally{wrapper.unmount()}
  92. })
  93. it('never switches an unavailable requested browser to another computer',async()=>{
  94. state.available=false;state.reason='执行节点离线'
  95. const wrapper=mount(ComputerPanel,{props:{agents:[agent],backend:'managed-browser',standalone:true,autoTake:true},global:{stubs}})
  96. try{
  97. await flushPromises()
  98. expect(wrapper.get('[role="alert"]').text()).toContain('所选电脑不可用')
  99. expect(request.mock.calls.some(([path])=>path.includes('/computer/take'))).toBe(false)
  100. expect(wrapper.find('.computer-screen img').exists()).toBe(false)
  101. }finally{wrapper.unmount()}
  102. })
  103. it('ignores browser metadata that arrives after a target switch',async()=>{
  104. state.open=true;state.tabs=[{id:'tab',title:'原页面',url:'https://example.com',active:true}]
  105. let release!:(value:unknown)=>void,count=0
  106. const original=request.getMockImplementation()!
  107. request.mockImplementation((path,options)=>path.endsWith('/managed-browser')&&++count===2?new Promise(resolve=>{release=resolve}):original(path,options))
  108. const wrapper=mount(ComputerPanel,{props:{agents:[agent],backend:'managed-browser',standalone:true},global:{stubs}})
  109. try{
  110. await flushPromises()
  111. await wrapper.get('[aria-label="切换电脑环境"]').setValue('desktop:local');await flushPromises()
  112. release({...state,tabs:[{id:'late',title:'延迟旧页面',url:'https://old.example',active:true}]});await flushPromises()
  113. expect(wrapper.findComponent(BrowserToolbar).exists()).toBe(false)
  114. expect(wrapper.text()).not.toContain('延迟旧页面')
  115. }finally{wrapper.unmount()}
  116. })
  117. it('accepts managed browser native viewer links but rejects a host mixed into them',async()=>{
  118. route.query={backend:'managed-browser'}
  119. const wrapper=mount(Viewer,{global:{stubs:{ComputerPanel:true}}})
  120. try{await flushPromises();expect(wrapper.getComponent(ComputerPanel).props()).toMatchObject({backend:'managed-browser',autoTake:true})}finally{wrapper.unmount()}
  121. route.query={backend:'managed-browser',host:'local'}
  122. const invalid=mount(Viewer,{global:{stubs:{ComputerPanel:true}}})
  123. try{await flushPromises();expect(invalid.text()).toContain('所选电脑无效');expect(invalid.findComponent(ComputerPanel).exists()).toBe(false)}finally{invalid.unmount()}
  124. })