web-acceptance.mjs 5.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import { chromium } from '@playwright/test';
  2. import { readFileSync, writeFileSync } from 'node:fs';
  3. const root='/tmp/bot-latency-acceptance', config=JSON.parse(readFileSync(root+'/connections.json'));
  4. const results=[], nonce=Date.now().toString(36);
  5. const browser=await chromium.launch({headless:true});
  6. try{
  7. for(const mode of ['baseline','optimized']){
  8. const {url,conversationId}=config[mode];
  9. const context=await browser.newContext({viewport:{width:1280,height:900}});
  10. const page=await context.newPage();
  11. await page.addInitScript(()=>{
  12. const NativeSource=window.EventSource;window.__latencySources=[];
  13. window.EventSource=class extends NativeSource { constructor(...args){super(...args);window.__latencySources.push(this);this.addEventListener('ready',()=>{this.__ready=true})} };
  14. const original=window.fetch;
  15. window.fetch=async(...args)=>{
  16. const input=args[0], options=args[1];const target=typeof input==='string'?input:input instanceof URL?input.href:input.url;
  17. const response=await original(...args);
  18. if(options?.method==='POST'&&/\/conversations\/[^/]+\/messages$/.test(target)) window.__botLatency?.push({time:Date.now()/1000,name:'post_ack',detail:''});
  19. return response;
  20. };
  21. });
  22. await fetch(`http://127.0.0.1:19359/scenario?delay=160&run=${mode}-web-setup`);
  23. await page.goto(url+'/conversations');
  24. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture');
  25. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass');
  26. const [loginResponse]=await Promise.all([page.waitForResponse(r=>r.url().endsWith('/api/app/login')&&r.request().method()==='POST'),page.getByRole('button',{name:'登录',exact:true}).click()]);
  27. await loginResponse.finished();if(!loginResponse.ok())throw new Error('Fixture login failed');
  28. await page.goto(url+'/conversations/'+conversationId);
  29. const input=page.locator('textarea.composer-textarea');
  30. await input.waitFor(); await page.evaluate(()=>document.fonts.ready);
  31. await page.evaluate(()=>{
  32. let previous='';
  33. const root=document.querySelector('.message-stack');
  34. new MutationObserver(()=>{
  35. const rows=document.querySelectorAll('.message--assistant');const row=rows[rows.length-1];
  36. const text=row?.querySelector('.message__content')?.textContent??'';const key=(row?.dataset.messageId??'')+'|'+text;
  37. if(key!==previous){previous=key;window.__botLatency.push({time:Date.now()/1000,name:'dom_text',detail:'assistant|'+(row?.querySelector('.markdown--streaming')?'streaming':'complete')+'|'+text.length});}
  38. }).observe(root,{subtree:true,characterData:true,childList:true});
  39. });
  40. for(const [kind,count] of [['short',6],['long',1]]){
  41. for(let i=1;i<=count;i++){
  42. const run=`${mode}-web-${kind}-${i}`, marker=`${kind}-${run}-${nonce}`;
  43. await fetch(`http://127.0.0.1:19359/scenario?delay=160&run=${run}`);
  44. await page.evaluate(()=>{window.__botLatency.length=0});
  45. await input.fill(`[latency:${marker}]`);await input.press('Enter');
  46. await page.waitForFunction(end=>window.__botLatency?.some(v=>v.name==='completed_text'&&v.detail.endsWith(end)),` END-${marker}`,{timeout:30000});
  47. const trace=await page.evaluate(()=>window.__botLatency);
  48. const completed=trace.find(v=>v.name==='completed_text'&&v.detail.endsWith(` END-${marker}`));
  49. const body=kind==='long'?Array.from({length:200},(_,n)=>`第${String(n+1).padStart(3,'0')}段。用于比较两个移动端的连续文本显示表现,全部内容来自隔离测试服务。\n\n`).join(''):Array.from({length:40},(_,n)=>`${String(n+1).padStart(2,'0')}测试文字 `).join('');
  50. if(completed.detail!==body+` END-${marker}`)throw new Error('Full response mismatch');
  51. results.push({run,mode,trace});writeFileSync(root+'/evidence/web-traces.json',JSON.stringify(results,null,2));
  52. if(i===count)await page.screenshot({path:root+`/evidence/${mode}-web-${kind}.png`});
  53. await page.waitForTimeout(700);
  54. }
  55. }
  56. if(mode==='optimized'){
  57. await fetch(`http://127.0.0.1:19359/scenario?delay=160&run=optimized-web-reading`);
  58. const scroller=page.locator('.message-stack').locator('..');const box=await scroller.boundingBox();
  59. await page.mouse.move(box.x+box.width/2,box.y+box.height/2);await page.mouse.wheel(0,-700);await page.waitForTimeout(200);
  60. const top=await scroller.evaluate(e=>e.scrollTop);
  61. const sources=await page.evaluate(()=>window.__latencySources.length);
  62. await page.evaluate(()=>window.__latencySources.at(-1).dispatchEvent(new MessageEvent('reset',{data:'{}'})));
  63. await page.waitForFunction(n=>window.__latencySources.length>n&&window.__latencySources.at(-1).__ready,sources);
  64. await page.waitForTimeout(100);
  65. if(Math.abs(await scroller.evaluate(e=>e.scrollTop)-top)>4)throw new Error('Snapshot recovery moved the reader');
  66. const caps=await(await context.request.get(url+'/api/app/capabilities')).json();const marker='short-reading-'+nonce;
  67. const sent=await context.request.post(url+'/api/app/conversations/'+conversationId+'/messages',{headers:{origin:url,'X-CSRF-Token':caps.csrfToken},data:{requestId:crypto.randomUUID(),content:'[latency:'+marker+']'}});
  68. if(!sent.ok())throw new Error(await sent.text());
  69. await page.waitForFunction(end=>window.__botLatency.some(v=>v.name==='completed_text'&&v.detail.endsWith(end)),' END-'+marker,{timeout:20000});
  70. if(Math.abs(await scroller.evaluate(e=>e.scrollTop)-top)>4)throw new Error('New reply pulled the reader to the bottom');
  71. await page.screenshot({path:root+'/evidence/optimized-web-reading.png'});
  72. }
  73. await context.close();
  74. }
  75. console.log('14 native Web UI sends passed with exact full text.');
  76. }finally{await browser.close()}