browserInstallationSmoke.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. // @vitest-environment node
  2. import {expect,it} from 'vitest'
  3. import {execFile} from 'node:child_process'
  4. import {mkdtemp,realpath,rm} from 'node:fs/promises'
  5. import {tmpdir} from 'node:os'
  6. import {join,resolve} from 'node:path'
  7. import {createRequire} from 'node:module'
  8. import {pathToFileURL} from 'node:url'
  9. import {promisify} from 'node:util'
  10. /** Opt-in real first-install test. It downloads the pinned Chromium into a fresh
  11. * temporary cache in a separate Node process, then removes that cache.
  12. * YAOYAO_BROWSER_INSTALL_SMOKE=1 npx vitest run tests/runner/browserInstallationSmoke.test.ts */
  13. it.skipIf(process.env.YAOYAO_BROWSER_INSTALL_SMOKE!=='1')('prepares an empty node browser cache, downloads Chromium and opens a sandboxed Runner session',async()=>{
  14. const root=await realpath(await mkdtemp(join(tmpdir(),'yaoyao-browser-install-smoke-')))
  15. const require=createRequire(import.meta.url)
  16. const script=`
  17. import {randomUUID} from 'node:crypto';
  18. import {existsSync} from 'node:fs';
  19. import {join} from 'node:path';
  20. import {RunnerBrowser} from ${JSON.stringify(pathToFileURL(resolve('src/runner/managedBrowser.ts')).href)};
  21. import {createBrowserInstaller} from ${JSON.stringify(pathToFileURL(resolve('src/runner/browser/installation.ts')).href)};
  22. const report=value=>process.stdout.write(JSON.stringify(value)+'\\n');
  23. const installer=createBrowserInstaller();
  24. const originalInstall=installer.install;
  25. installer.install=options=>originalInstall({...options,onUpdate:update=>{report({event:'progress',...update});options.onUpdate(update)}});
  26. const grants=new Map();
  27. const check=async(_scope,id)=>{if(id?grants.has(id):[...grants.values()].some(role=>role!=='prepare'))return;throw new Error('browser session has no active grant')};
  28. const browser=new RunnerBrowser({enabled:true},process.env.YAOYAO_INSTALL_SMOKE_HOME,check,{installer});
  29. process.once('SIGTERM',()=>void browser.shutdown().finally(()=>process.exit(1)));
  30. const scope={ownerKey:'a'.repeat(64),environmentId:randomUUID(),profile:'temporary'},setupGrant=randomUUID(),taskGrant=randomUUID();
  31. const invoke=async(op,extra={})=>{
  32. const role=op==='status'?'view':op==='prepare'?'prepare':'task',grantId=role==='view'?randomUUID():role==='prepare'?setupGrant:taskGrant;
  33. grants.set(grantId,role);
  34. try{return await browser.invoke({scope,profile:'default',grantId,op,...extra},Date.now()+30000,()=>{})}
  35. finally{if(role==='view')grants.delete(grantId)}
  36. };
  37. try {
  38. await browser.ready;
  39. if(browser.available||existsSync(process.env.PLAYWRIGHT_BROWSERS_PATH))throw new Error('fixture cache is not empty');
  40. report({event:'initial',...await invoke('status')});
  41. report({event:'prepared',...await invoke('prepare')});
  42. let state;
  43. for(;;){
  44. state=await invoke('status');
  45. if(state.installation.status==='ready')break;
  46. if(state.installation.status==='failed')throw new Error(state.installation.message);
  47. await new Promise(resolve=>setTimeout(resolve,200));
  48. }
  49. grants.delete(setupGrant);
  50. if(!await installer.probe())throw new Error('post-install sandbox probe failed');
  51. const opened=await invoke('open');
  52. const frame=await invoke('execute',{operation:{generation:opened.generation,operationId:'installation-smoke',action:{kind:'screenshot'}}});
  53. if(Buffer.from(frame.data,'base64').subarray(1,4).toString()!=='PNG')throw new Error('missing browser frame');
  54. const {chromium}=await import('playwright');
  55. report({event:'ready',available:state.available,status:state.installation.status,open:opened.open,mimeType:frame.mimeType,executableInTemporaryCache:chromium.executablePath().startsWith(process.env.PLAYWRIGHT_BROWSERS_PATH)});
  56. await invoke('close');
  57. }finally{await browser.shutdown()}
  58. `
  59. try{
  60. const {stdout}=await promisify(execFile)(process.execPath,['--import',require.resolve('tsx'),'--input-type=module','--eval',script],{
  61. cwd:resolve('.'),env:{...process.env,PLAYWRIGHT_BROWSERS_PATH:join(root,'browsers'),YAOYAO_INSTALL_SMOKE_HOME:join(root,'runner'),ELECTRON_RUN_AS_NODE:'1'},timeout:9*60000,maxBuffer:1024*1024,
  62. })
  63. const events=stdout.trim().split('\n').map(line=>JSON.parse(line))
  64. expect(events.find(event=>event.event==='initial')).toMatchObject({available:false,installation:{status:'missing'}})
  65. expect(events.find(event=>event.event==='prepared')).toMatchObject({available:false,installation:{status:'installing'}})
  66. expect(events.filter(event=>event.event==='progress').map(event=>event.message)).toContain('正在验证 Chromium 沙箱启动')
  67. expect(events.find(event=>event.event==='ready')).toMatchObject({available:true,status:'ready',open:true,mimeType:'image/png',executableInTemporaryCache:true})
  68. // Keep the evidence useful without exposing the temporary cache's personal path.
  69. console.info('fresh-install smoke evidence:',JSON.stringify(events.filter(event=>['progress','ready'].includes(event.event))))
  70. }finally{await rm(root,{recursive:true,force:true})}
  71. },10*60000)