inspect-S4-browser-display.mjs 7.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. // Fresh anonymous production browser. Public reads only; no application overrides.
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import assert from 'node:assert/strict';
  5. import {spawn} from 'node:child_process';
  6. const verified=JSON.parse(fs.readFileSync('work/live-S4-readback-20261103.json','utf8'));
  7. const template=JSON.parse(fs.readFileSync('work/S4-classroom-next-20261103/05-project-S4-save.template.json','utf8'));
  8. assert.ok(verified.ok);assert.equal(verified.artifactId,'U3_W7WDliP');
  9. const base='https://kangshifu.fmode.cn',output='work/live-S4-display-inspection-20261103.json',runId=Date.now();
  10. assert.equal(fs.existsSync(output),false,'Preserve earlier browser acceptance');
  11. const profile=path.resolve('work/browser-S4-'+runId);fs.mkdirSync(profile);
  12. const browser=spawn('C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',[
  13. '--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check','--disable-background-networking',
  14. '--remote-debugging-address=127.0.0.1','--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'
  15. ],{windowsHide:true,stdio:'ignore'});
  16. const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
  17. const checks=[],errors=[],requests=[],observations=[],screenshots=[];
  18. let socket;
  19. try{
  20. let port;
  21. for(let i=0;i<100;i++){try{port=Number(fs.readFileSync(path.join(profile,'DevToolsActivePort'),'utf8').split('\n')[0]);}catch{}if(port)break;await sleep(100);}
  22. assert.ok(port,'browser debugger');
  23. const tabs=await(await fetch('http://127.0.0.1:'+port+'/json/list')).json();
  24. socket=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl);
  25. await new Promise((resolve,reject)=>{socket.addEventListener('open',resolve,{once:true});socket.addEventListener('error',reject,{once:true});});
  26. let seq=0;const pending=new Map();
  27. socket.addEventListener('message',({data})=>{
  28. const m=JSON.parse(data);
  29. 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);}
  30. if(m.method==='Runtime.exceptionThrown')errors.push({runtime:m.params.exceptionDetails.text});
  31. if(m.method==='Network.loadingFailed'&&m.params.errorText!=='net::ERR_ABORTED')errors.push({network:m.params.errorText});
  32. if(m.method==='Log.entryAdded'&&m.params.entry.level==='error'&&!m.params.entry.url?.endsWith('/favicon.ico'))errors.push({console:m.params.entry.text});
  33. if(m.method==='Network.requestWillBeSent'){
  34. const r=m.params.request;let cloudPath=null;
  35. if(r.url.endsWith('/api/functions')){try{cloudPath=JSON.parse(r.postData||'{}').path??null;}catch{}}
  36. requests.push({url:r.url,method:r.method,headerNames:Object.keys(r.headers||{}),cloudPath});
  37. }
  38. });
  39. const cdp=(method,params={})=>new Promise((resolve,reject)=>{
  40. const id=++seq,timer=setTimeout(()=>{pending.delete(id);reject(Error(method+' timeout'));},30000);
  41. pending.set(id,{resolve,reject,timer});socket.send(JSON.stringify({id,method,params}));
  42. });
  43. const evaluate=async expression=>{
  44. const r=await cdp('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});
  45. if(r.exceptionDetails)throw Error(r.exceptionDetails.text);return r.result.value;
  46. };
  47. const check=(name,passed)=>{checks.push({name,passed:Boolean(passed)});assert.ok(passed,name);};
  48. const helpers=`function find(root,selector){const hit=root.querySelector(selector);if(hit)return hit;for(const n of root.querySelectorAll('*'))if(n.shadowRoot){const found=find(n.shadowRoot,selector);if(found)return found;}return null;}
  49. function text(root){let out='';for(const n of root.childNodes){if(n.nodeType===Node.TEXT_NODE)out+=n.textContent;else if(n.nodeType===Node.ELEMENT_NODE&&!['STYLE','SCRIPT','NOSCRIPT','TEMPLATE'].includes(n.tagName)){out+=text(n);if(n.shadowRoot)out+=text(n.shadowRoot);}}return out;}
  50. function tableSection(title){const view=find(document,'view-report');return [...view.shadowRoot.querySelectorAll('section.sec')].find(s=>s.querySelector('h4')?.textContent.trim()===title);}`;
  51. const deepText=()=>evaluate(`(()=>{${helpers}return text(document);})()`);
  52. const compact=x=>x.replace(/\s/g,'');
  53. const displayText=x=>x.replace(/\u2009/g,'');
  54. const screenshot=async label=>{
  55. const shot=await cdp('Page.captureScreenshot',{format:'png'}),file='work/live-S4-'+label+'-'+runId+'.png';
  56. fs.writeFileSync(file,Buffer.from(shot.data,'base64'),{flag:'wx'});screenshots.push(file);
  57. };
  58. const navigate=async route=>{
  59. await cdp('Page.navigate',{url:base+route});let ready=false;
  60. for(let i=0;i<120;i++){await sleep(200);try{ready=await evaluate("import('/src/services/board-service.js').then(m=>m.boardService.status.get()==='ready')");}catch{}if(ready)break;}
  61. check(route+' public data loaded',ready);await sleep(250);
  62. };
  63. const click=async point=>{
  64. await cdp('Input.dispatchMouseEvent',{type:'mousePressed',...point,button:'left',clickCount:1});
  65. await cdp('Input.dispatchMouseEvent',{type:'mouseReleased',...point,button:'left',clickCount:1});
  66. };
  67. await cdp('Page.enable');await cdp('Runtime.enable');await cdp('Network.enable');await cdp('Log.enable');
  68. await cdp('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:true});
  69. await navigate('/report/'+verified.artifactId);
  70. await evaluate(`(()=>{${helpers}find(document,'[data-anchor="public-p1"]').scrollIntoView({block:'center'});})()`);
  71. await sleep(250);
  72. const point=await evaluate(`(()=>{${helpers}const r=find(document,'[data-anchor="public-p1"]').getBoundingClientRect();return {x:r.left+r.width/2,y:r.top+r.height/2};})()`);
  73. await click(point);await sleep(350);
  74. const display=await evaluate(`(()=>{${helpers}const host=find(document,'kf-drawer'),body=host.shadowRoot.querySelector('.body'),prov=body.querySelector('.prov'),metrics=body.querySelector('.kv');return {open:host.open,metricLabels:[...metrics.querySelectorAll('div')].map(n=>n.textContent),sourceRef:prov.textContent,body:{clientWidth:body.clientWidth,scrollWidth:body.scrollWidth,overflowX:getComputedStyle(body).overflowX},page:{clientWidth:document.documentElement.clientWidth,scrollWidth:document.documentElement.scrollWidth}};})()`);
  75. const publicModel=await evaluate("import('/src/services/board-service.js').then(({boardService})=>({ownerCount:boardService.db.get().artifactById['U3_W7WDliP'].report.evidence['public-p1'].ownerCount??null}))");
  76. check('owner count remains unknown in public evidence model',publicModel.ownerCount===null);
  77. check('unknown owner count shown as dash, not numeric one',display.metricLabels[0].replace(/\s/g,'')==='—人独立提到');
  78. check('P1 drawer open',display.open);
  79. const scroll=await evaluate(`(()=>{${helpers}const b=find(document,'kf-drawer').shadowRoot.querySelector('.body');b.scrollLeft=b.scrollWidth;return {scrollLeft:b.scrollLeft,maxScroll:b.scrollWidth-b.clientWidth};})()`);
  80. await screenshot('source-ref-display-mobile');
  81. const result={ok:true,at:new Date().toISOString(),operation:'S4-targeted-display-inspection',readOnly:true,businessWrites:0,checks,display,publicModel,horizontalScroll:scroll,screenshots,
  82. notes:['The glyph was a dash for unknown ownerCount, not a count of one. Generic person wording remains inappropriate for a retail listing.','A long provenance URL makes the drawer body horizontally scrollable on 390px; page width remains 390px.']};
  83. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.log(JSON.stringify(result,null,2));
  84. }catch(error){
  85. const result={ok:false,at:new Date().toISOString(),error:error.message,checks,errors,readOnly:true,businessWrites:0};
  86. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result));process.exitCode=1;
  87. }finally{socket?.close();browser.kill();}