browserSmoke.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. // @vitest-environment node
  2. import {expect,it} from 'vitest'
  3. import {mkdtemp,realpath,rm} from 'node:fs/promises'
  4. import {tmpdir} from 'node:os'
  5. import {join} from 'node:path'
  6. import {chromium,type BrowserContext,type Page} from 'playwright'
  7. import {BrowserRuntime,type BrowserLauncher,type BrowserScope,type BrowserAction} from '../../src/runner/browser/runtime.js'
  8. /** YAOYAO_BROWSER_SMOKE=1 npx vitest run tests/runner/browserSmoke.test.ts
  9. * Uses real Chromium and disk profiles, but page-level fixtures never contact a
  10. * website. Public proxy policy is exercised separately with real local sockets. */
  11. it.skipIf(process.env.YAOYAO_BROWSER_SMOKE!=='1')('real Chromium: DOM refs, takeover, uploads/downloads and persistent login across runtime restart',async()=>{
  12. const root=await realpath(await mkdtemp(join(tmpdir(),'yaoyao-browser-smoke-')))
  13. const scope:BrowserScope={ownerKey:'smoke-account',environmentId:'smoke-bot',profile:'persistent'}
  14. const contexts:BrowserContext[]=[],calls:string[]=[]
  15. const html=`<!doctype html><html><body>
  16. <label>Name <input id="name" aria-label="Name"></label>
  17. <button id="apply" onclick="document.querySelector('#result').textContent=document.querySelector('#name').value">Apply</button>
  18. <button id="login" onclick="document.cookie='login=ok; Max-Age=86400; SameSite=Lax; path=/';document.querySelector('#result').textContent='logged in'">Login</button>
  19. <input id="file" type="file" aria-label="Upload" onchange="this.files[0].text().then(text=>document.querySelector('#uploaded').textContent=text)">
  20. <a href="/download">Download</a><output id="result"></output><output id="uploaded"></output>
  21. </body></html>`
  22. const prepare=async(page:Page)=>page.route('https://browser.fixture.test/**',async route=>{
  23. if(new URL(route.request().url()).pathname==='/download')await route.fulfill({status:200,headers:{'Content-Type':'text/plain','Content-Disposition':'attachment; filename="report.txt"'},body:'controlled download'})
  24. else await route.fulfill({status:200,contentType:'text/html',body:html})
  25. })
  26. const launcher:BrowserLauncher={launchPersistentContext:async(directory,options)=>{
  27. const context=await chromium.launchPersistentContext(directory,options);contexts.push(context)
  28. for(const page of context.pages())await prepare(page)
  29. context.on('page',page=>void prepare(page))
  30. return context
  31. }}
  32. const create=()=>new BrowserRuntime({root,enabled:true,launcher,authorize:async()=>{calls.push('scope')},resolveUpload:async(_scope,fileId)=>{
  33. expect(fileId).toBe('attachment-id');return {name:'approved.txt',mimeType:'text/plain',buffer:Buffer.from('controlled upload')}
  34. }})
  35. let runtime=create(),generation=0,sequence=0
  36. const execute=(action:BrowserAction)=>runtime.execute(scope,{generation,operationId:'smoke-'+(++sequence),action},{authorize:async()=>{calls.push('actor')}})
  37. type Snapshot={snapshotId:string;elements:{ref:string;tag:string;type:string;text:string;label:string}[]}
  38. const snapshot=async()=>await execute({kind:'snapshot'}) as Snapshot
  39. try{
  40. generation=(await runtime.open(scope,{authorize:async()=>{calls.push('task')}})).generation
  41. await execute({kind:'navigate',url:'https://browser.fixture.test/'})
  42. const first=await snapshot();await execute({kind:'fill',snapshotId:first.snapshotId,ref:first.elements.find(e=>e.label==='Name')!.ref,text:'before takeover'})
  43. const second=await snapshot();await execute({kind:'click',snapshotId:second.snapshotId,ref:second.elements.find(e=>e.text==='Apply')!.ref})
  44. const page=contexts[0]!.pages()[0]!
  45. expect(await page.locator('#result').innerText()).toBe('before takeover')
  46. const before=contexts[0],oldGeneration=generation
  47. generation=(await runtime.pause(scope,{authorize:async()=>{calls.push('human')}})).generation
  48. expect(contexts[0]).toBe(before);expect(contexts).toHaveLength(1)
  49. await expect(runtime.execute(scope,{generation:oldGeneration,operationId:'stale',action:{kind:'text',text:'stale'}})).rejects.toMatchObject({code:'browser_generation_expired'})
  50. const login=await snapshot();await execute({kind:'click',snapshotId:login.snapshotId,ref:login.elements.find(e=>e.text==='Login')!.ref})
  51. expect((await contexts[0]!.cookies()).find(c=>c.name==='login')?.value).toBe('ok')
  52. const retained=(await runtime.status(scope)).tabs
  53. const parked=await runtime.park(scope,{authorize:async()=>{calls.push('retention')}})
  54. expect(parked.open).toBe(true);expect(parked.tabs).toEqual(retained)
  55. expect(await page.locator('#name').inputValue()).toBe('before takeover')
  56. expect(await page.locator('#result').innerText()).toBe('logged in')
  57. expect(contexts).toHaveLength(1)
  58. await expect(runtime.execute(scope,{generation:parked.generation,operationId:'idle-input',action:{kind:'text',text:'forbidden'}})).rejects.toMatchObject({code:'browser_parked'})
  59. generation=(await runtime.resume(scope,{authorize:async()=>{calls.push('task')}})).generation
  60. expect((await runtime.status(scope)).tabs).toEqual(retained)
  61. expect(await page.locator('#name').inputValue()).toBe('before takeover')
  62. const upload=await snapshot();await execute({kind:'upload',snapshotId:upload.snapshotId,ref:upload.elements.find(e=>e.type==='file')!.ref,fileId:'attachment-id'})
  63. await expect.poll(()=>page.locator('#uploaded').innerText()).toBe('controlled upload')
  64. const download=await snapshot();await execute({kind:'click',snapshotId:download.snapshotId,ref:download.elements.find(e=>e.text==='Download')!.ref})
  65. await expect.poll(async()=> (await runtime.status(scope)).downloads.length).toBe(1)
  66. const metadata=(await runtime.status(scope)).downloads[0]!,file=await runtime.readDownload(scope,metadata.id,{authorize:async()=>{calls.push('export')}})
  67. expect(file.buffer.toString()).toBe('controlled download');expect(file.name).toBe('report.txt')
  68. const frame=await execute({kind:'screenshot'}) as {data:string;mimeType:string}
  69. expect(frame.mimeType).toBe('image/png');expect(Buffer.from(frame.data,'base64').subarray(1,4).toString()).toBe('PNG')
  70. await runtime.shutdown();runtime=create()
  71. generation=(await runtime.open(scope)).generation
  72. await execute({kind:'navigate',url:'https://browser.fixture.test/'})
  73. expect((await contexts[1]!.cookies()).find(c=>c.name==='login')?.value).toBe('ok')
  74. expect((await runtime.status(scope)).downloads).toEqual([])
  75. expect(calls).toContain('actor');expect(calls).toContain('human');expect(calls).toContain('export')
  76. }finally{await runtime.shutdown();await rm(root,{recursive:true,force:true})}
  77. },60000)