verify-workshop-browser.mjs 6.4 KB

12345678910111213141516171819202122232425262728293031323334353637
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import assert from 'node:assert/strict';
  4. import {spawn} from 'node:child_process';
  5. const base=process.argv[2]||'http://127.0.0.1:4173';
  6. const live=JSON.parse(fs.readFileSync('work/workshop-live.json'));
  7. const profile=path.resolve('work/browser-workshop-'+Date.now());fs.mkdirSync(profile,{recursive:true});
  8. const browser=spawn('C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',['--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check','--disable-background-networking','--remote-debugging-address=127.0.0.1','--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'],{windowsHide:true,stdio:'ignore'});
  9. const sleep=ms=>new Promise(r=>setTimeout(r,ms));let socket;const checks=[],errors=[],requests=[];
  10. try{
  11. let port;for(let i=0;i<100;i++){try{port=Number(fs.readFileSync(profile+'/DevToolsActivePort','utf8').split('\n')[0]);}catch{}if(port)break;await sleep(100);}assert(port);
  12. const tabs=await(await fetch('http://127.0.0.1:'+port+'/json/list')).json();socket=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl);await new Promise((res,rej)=>{socket.addEventListener('open',res,{once:true});socket.addEventListener('error',rej,{once:true});});
  13. let seq=0;const pending=new Map();socket.addEventListener('message',({data})=>{const m=JSON.parse(data);if(m.id&&pending.has(m.id)){const p=pending.get(m.id);clearTimeout(p.timer);pending.delete(m.id);m.error?p.reject(Error(m.error.message)):p.resolve(m.result);}if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.text);if(m.method==='Network.requestWillBeSent')requests.push({url:m.params.request.url,method:m.params.request.method});});
  14. socket.addEventListener('message',({data})=>{const m=JSON.parse(data);if(m.method==='Network.loadingFailed')errors.push({network:m.params.errorText,type:m.params.type,cors:m.params.corsErrorStatus});if(m.method==='Log.entryAdded'&&m.params.entry.level==='error'&&!m.params.entry.url?.endsWith('/favicon.ico'))errors.push({console:m.params.entry.text,url:m.params.entry.url});});
  15. const cdp=(method,params={})=>new Promise((resolve,reject)=>{const id=++seq,timer=setTimeout(()=>{pending.delete(id);reject(Error(method+' timeout'));},30000);pending.set(id,{resolve,reject,timer});socket.send(JSON.stringify({id,method,params}));});
  16. const evaluate=async expression=>{const r=await cdp('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(r.exceptionDetails.text);return r.result.value;};
  17. const check=(name,ok)=>{checks.push({name,passed:Boolean(ok)});assert(ok,name);};
  18. await cdp('Page.enable');await cdp('Runtime.enable');await cdp('Network.enable');await cdp('Log.enable');await cdp('Emulation.setDeviceMetricsOverride',{width:1440,height:1000,deviceScaleFactor:1,mobile:false});
  19. const deep="(()=>{const out=[];function read(root){for(const n of root.querySelectorAll('*')){if(n.shadowRoot)read(n.shadowRoot);}out.push(root.textContent||'');}read(document);return out.join(' ').replace(/[\\s\\u2009\\u00a0]/g,'');})()";
  20. for(const route of ['/','/members','/stages','/report/'+live.artifactId]){
  21. await cdp('Page.navigate',{url:base+route});let ready=false;
  22. for(let i=0;i<120;i++){await sleep(200);ready=await evaluate("import('/src/services/board-service.js').then(m=>m.boardService.status.get()==='ready')");if(ready)break;}if(!ready)errors.push(await evaluate("import('/src/services/board-service.js').then(m=>({status:m.boardService.status.get(),error:m.boardService.error.get(),url:location.href,title:document.title}))").catch(e=>({evaluateError:e.message})));check(route+' 数据加载',ready);
  23. const text=await evaluate(deep);check(route+' 有联调员工或报告',text.includes('工作坊联调(非学员)')||text.includes('新品方案与待验证事项'));
  24. if(route==='/members'){
  25. const row=await evaluate("document.querySelector('kf-app').shadowRoot.querySelector('view-members').shadowRoot.querySelector('[data-project=\""+live.projectId+"\"]')?.textContent.replace(/[\\s\\u2009\\u00a0]/g,'')");
  26. check('逐项目进展含负责人、阶段与课堂完成状态',row?.includes('工作坊联调(非学员)')&&row.includes('S5')&&row.includes('课堂完成'));
  27. await cdp('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:true});await sleep(200);check('390px 成员及项目无横向溢出',await evaluate('document.documentElement.scrollWidth<=innerWidth'));
  28. const shot=await cdp('Page.captureScreenshot',{format:'png',captureBeyondViewport:true});fs.writeFileSync('work/workshop-members-mobile.png',Buffer.from(shot.data,'base64'));
  29. await cdp('Emulation.setDeviceMetricsOverride',{width:1440,height:1000,deviceScaleFactor:1,mobile:false});
  30. }
  31. if(route.startsWith('/report/')){check('报告有实际 Agent 名称',text.includes('王刚001(联调模拟)'));check('报告明确样例身份',text.includes('样例'));check('报告正文已渲染',text.includes('本阶段产出'));const shot=await cdp('Page.captureScreenshot',{format:'png'});fs.writeFileSync('work/workshop-report-desktop.png',Buffer.from(shot.data,'base64'));}
  32. check(route+' 无横向溢出',await evaluate('document.documentElement.scrollWidth<=innerWidth'));
  33. }
  34. await cdp('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:true});await sleep(300);check('390px 报告无横向溢出',await evaluate('document.documentElement.scrollWidth<=innerWidth'));const shot=await cdp('Page.captureScreenshot',{format:'png'});fs.writeFileSync('work/workshop-report-mobile.png',Buffer.from(shot.data,'base64'));
  35. check('未加入登录页面',!await evaluate("Boolean(document.querySelector('input[type=password]'))"));check('无运行异常',errors.length===0);check('通过云函数公开读取',requests.some(x=>x.url.endsWith('/api/functions')&&x.method==='POST'));check('不直接扫描 Parse 旧表',!requests.some(x=>x.url.includes('/parse/classes/')));
  36. const out={at:new Date().toISOString(),base,ok:checks.every(x=>x.passed),checks,errors,cloudReadRequests:requests.filter(x=>x.url.endsWith('/api/functions')).length,businessWrites:0};fs.writeFileSync('work/workshop-browser.json',JSON.stringify(out,null,2));console.log(JSON.stringify(out,null,2));
  37. }catch(e){fs.writeFileSync('work/workshop-browser.json',JSON.stringify({at:new Date().toISOString(),base,ok:false,checks,errors,error:e.message},null,2));throw e;}finally{socket?.close();browser.kill();}