135 lines
16 KiB
JavaScript
135 lines
16 KiB
JavaScript
const fs=require('node:fs/promises'),path=require('node:path'),assert=require('node:assert/strict')
|
||
const output=path.resolve('.tmp/table-ui-regression')
|
||
const experiment=process.env.YCODE_TABLE_DISABLE_LCD_EXPERIMENT==='1'
|
||
const layerCase=process.env.YCODE_TABLE_LAYER_CASE||'baseline'
|
||
assert.ok(['baseline','source-scroll-flow','source-scroll-no-container','source-clip-flow'].includes(layerCase))
|
||
const suffix=(layerCase==='baseline'?'':'-'+layerCase)+(experiment?'-lcd-disabled-experiment':'')
|
||
if(!process.versions.electron){
|
||
;(async()=>{
|
||
await fs.mkdir(output,{recursive:true})
|
||
const markdown='| 路径 | 持久化状态 | 内容 |\n| --- | --- | --- |\n| 取消 | `cancelled` ✓ | 0 字符 |\n| 正常 | `done` ✓ | 「收到」 |'
|
||
const sourceLayout=JSON.parse(await fs.readFile('.tmp/rea-assessment-initial/layout.json','utf8'))
|
||
assert.equal(sourceLayout.evidenceId,'ev_d6e6920ff4e47f73a182fac5312c5f02393366a035445d18c8a6733a8c8a11ed')
|
||
const contentNodes=sourceLayout.message_content.nodes
|
||
const paragraph=contentNodes.filter(node=>node.node_index>=1622&&node.node_index<1630&&node.node_name==='#text').map(node=>node.text).join('')
|
||
const sourceMarkdown='✅ **取消链路修复并双验证通过**:\n\n'+markdown+'\n\n'+paragraph
|
||
const originalFixture=`<div data-table-native-source-flow style={{position:'absolute',inset:0,transform:'translateY(11220px)'}}><div className="chat-pane cr-theme" data-cr-theme="light" style={{position:'absolute',left:324.28125,top:-10834.671875,width:436.703125,height:'auto',minHeight:0,display:'block',overflow:'visible'}}><MarkdownContent content={source} messageId="fixture-message" sourceId="fixture-message-text-0" onQuote={quote=>window.qa.quoted.push(quote)}/></div></div>`
|
||
const sourceFixture=`<div className="chat-pane cr-theme" data-cr-theme="light" style={{position:'absolute',inset:0,display:'block',overflow:'visible'}}>
|
||
<div className="cr-message-list-viewport" data-source-node="772" style={{position:'absolute',left:284.28125,top:94,width:526.703125,height:639}}>
|
||
<div className="cr-message-list cr-message-list--virtual-anchored" data-source-scroll data-source-node="773" style={{overflow:${JSON.stringify(layerCase==='source-clip-flow'?'hidden':'auto')}}}>
|
||
<div data-source-node="774" className="cr-message-list__content cr-message-list__content--revealed" style={{height:15886.828125,paddingLeft:40,paddingRight:40,maxWidth:832,...(${JSON.stringify(layerCase)}==='source-clip-flow'?{position:'relative',top:-15247,width:516.703125}: {})}}>
|
||
<div className="cr-document" data-source-node="775" style={{height:15886.828125}}><div className="cr-document__virtual-spacer" data-source-node="776" style={{height:15886.828125}}>
|
||
<div className="cr-document__virtual-flow" data-source-node="777" style={{height:4666.828125,transform:'translateY(11220px)'}}><div style={{height:4666.828125}}><div style={{height:4666.828125}}>
|
||
<div style={{height:4189.328125}}/>
|
||
<div className="cr-document__virtual-item cr-document__virtual-item--flow" data-source-node="1545" style={{height:477.5}}>
|
||
<div className="cr-frame cr-frame--left" style={{height:477.5,paddingTop:16,paddingBottom:16}}><div className="cr-frame__content" style={{height:445.5}}><div className="cr-agent" style={{height:445.5}}>
|
||
<div style={{height:36.5}}/><div className="cr-agent__body-row" style={{height:409}}><div className="cr-agent__body" style={{height:409}}><div className="cr-agent__content" style={{height:293.75}}>
|
||
<div style={{height:26.25}}/><div className="cr-text-block cr-text-block--spaced-top" style={{marginTop:12}}><MarkdownContent content={sourceMarkdown} messageId="source-message" sourceId="source-message-text-0" onQuote={quote=>window.qa.quoted.push(quote)}/></div>
|
||
</div></div></div>
|
||
</div></div></div>
|
||
</div>
|
||
</div></div></div>
|
||
</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`
|
||
const entry=`import React from 'react';import {createRoot} from 'react-dom/client';
|
||
import MarkdownContent from './src/renderer/src/components/MarkdownContent';
|
||
import {ReactMarkdown} from './src/renderer/src/workbuddy/markdown/ReactMarkdown';import {ReactMarkdownProvider} from './src/renderer/src/workbuddy/markdown/context';import {createMarkdownParser} from './src/renderer/src/workbuddy/markdown/parser';
|
||
import './src/renderer/src/workbuddy/typography/tokens.css';import './src/renderer/src/styles/tokens.css';import './src/renderer/src/styles/app.css';
|
||
import './src/renderer/src/workbuddy/conversation/source-styles.css';import './src/renderer/src/workbuddy/conversation/source-adapters.css';
|
||
const source=${JSON.stringify(markdown)};
|
||
const sourceMarkdown=${JSON.stringify(sourceMarkdown)};
|
||
window.qa={quoted:[],opened:[]};const root=createRoot(document.getElementById('root'));
|
||
root.render(${layerCase==='baseline'?originalFixture:sourceFixture});
|
||
window.qa.prepareSourceLayers=()=>{const scroll=document.querySelector('[data-source-scroll]');if(scroll&&${JSON.stringify(layerCase)}!=='source-clip-flow')scroll.scrollTop=15247};
|
||
window.qa.mountPath=()=>root.render(<div className="chat-pane cr-theme" data-cr-theme="light" style={{position:'absolute',left:324,top:385,width:200,height:'auto',display:'block',overflow:'visible'}}><ReactMarkdownProvider value={{mdInstance:createMarkdownParser({loading:true}),requestId:'table-path-fixture',resolveCode:async()=> 'file',pathClickHandler:{onPathClick:(path)=>window.qa.opened.push(path)}}}><ReactMarkdown isFinished>{'| file |\\n| --- |\\n| '+String.fromCharCode(96)+'C:/fixture/very-long-demo-file-to-check-horizontal-scroll.ts'+String.fromCharCode(96)+' |'}</ReactMarkdown></ReactMarkdownProvider></div>);
|
||
`
|
||
await require('esbuild').build({stdin:{contents:entry,loader:'tsx',resolveDir:process.cwd()},outfile:path.join(output,'fixture.js'),bundle:true,platform:'browser',format:'iife',jsx:'automatic',logLevel:'silent',loader:{'.woff':'file','.woff2':'file','.ttf':'file'}})
|
||
if(layerCase!=='baseline'){
|
||
const css=await fs.readFile('.tmp/rea-css-recovered/src-CyXHA0IC.css','utf8'),rules=[]
|
||
assert.equal(require('node:crypto').createHash('sha256').update(css).digest('hex'),'a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe')
|
||
require('postcss').parse(css).walkRules(rule=>{if(!/^\.cr-document(?:\b|__virtual)|^\.cr-message-list/.test(rule.selector)||rule.selector.includes('__hydration'))return;const copy=rule.clone();if(layerCase==='source-scroll-no-container')copy.walkDecls('container-type',decl=>decl.remove());rules.push(copy.toString())})
|
||
await fs.appendFile(path.join(output,'fixture.css'),'\n/* Pinned REA ancestor rules, fixture only. */\n'+rules.join('\n'))
|
||
}
|
||
await fs.writeFile(path.join(output,'index.html'),'<!doctype html><html data-theme="light"><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"><style>html,body,#root{width:1336px;height:901px;margin:0;background:white}</style><div id="root"></div><script src="fixture.js"></script></html>')
|
||
const env={...process.env};delete env.ELECTRON_RUN_AS_NODE
|
||
const result=require('node:child_process').spawnSync(require('electron'),[__filename],{env,windowsHide:true,stdio:'inherit',timeout:45000})
|
||
process.exitCode=result.status??1
|
||
})().catch(error=>{console.error(error);process.exitCode=1})
|
||
}else{
|
||
const {app,BrowserWindow,nativeImage}=require('electron')
|
||
app.setPath('userData',path.join(output,'user-data'))
|
||
app.disableHardwareAcceleration()
|
||
app.commandLine.appendSwitch('use-angle','swiftshader')
|
||
app.commandLine.appendSwitch('disable-gpu-compositing')
|
||
if(experiment)app.commandLine.appendSwitch('disable-lcd-text')
|
||
app.whenReady().then(async()=>{
|
||
const errors=[],win=new BrowserWindow({x:0,y:0,width:1336,height:901,useContentSize:true,frame:false,show:false,webPreferences:{contextIsolation:true,nodeIntegration:false,offscreen:false,backgroundThrottling:false}})
|
||
win.webContents.on('console-message',event=>{if(event.level==='error')errors.push(event.message)})
|
||
await win.loadFile(path.join(output,'index.html'))
|
||
await new Promise(resolve=>setTimeout(resolve,450))
|
||
await win.webContents.executeJavaScript('window.qa.prepareSourceLayers()')
|
||
await new Promise(resolve=>setTimeout(resolve,50))
|
||
const metrics=await win.webContents.executeJavaScript(`(()=>{const root=document.querySelector('.cr-table-block');const selectors=['.cr-table-block__inner','table','th','td','code'];const styles=['display','font-family','font-size','font-weight','line-height','color','background-color','border-radius','border-top-width','border-top-color','padding-top','padding-right','padding-bottom','padding-left'];return {nodes:Object.fromEntries(selectors.map(selector=>{const e=root.querySelector(selector),c=getComputedStyle(e);return [selector,{bounds:e.getBoundingClientRect().toJSON(),styles:Object.fromEntries(styles.map(key=>[key,c.getPropertyValue(key)]))}]})),rows:[...root.querySelectorAll('tr')].map(e=>e.getBoundingClientRect().height),cell:root.querySelector('tbody tr td:nth-child(2)').textContent}})()`)
|
||
assert.equal(metrics.nodes['.cr-table-block__inner'].styles['border-radius'],'12px')
|
||
assert.equal(metrics.nodes.th.styles['padding-top'],'8px')
|
||
assert.equal(metrics.nodes.th.styles['padding-right'],'12px')
|
||
assert.equal(metrics.nodes.code.styles['background-color'],'rgba(0, 0, 0, 0.05)')
|
||
assert.equal(metrics.cell,'cancelled ✓')
|
||
const source=JSON.parse(await fs.readFile('.tmp/rea-assessment-initial/layout.json','utf8'))
|
||
assert.equal(source.evidenceId,'ev_d6e6920ff4e47f73a182fac5312c5f02393366a035445d18c8a6733a8c8a11ed')
|
||
const sourceNodes=source.layout.documents[0].nodes
|
||
for(const [selector,index] of [['.cr-table-block__inner',1592],['table',1593],['th',1596],['td',1604],['code',1607]]){
|
||
const expected=sourceNodes.find(node=>node.node_index===index),actual=metrics.nodes[selector]
|
||
assert.deepEqual([actual.bounds.x,actual.bounds.y,actual.bounds.width,actual.bounds.height],expected.bounds,selector+' source bounds')
|
||
for(const [name,value] of Object.entries(actual.styles))assert.equal(value,expected.styles[name],selector+' source '+name)
|
||
}
|
||
const ancestorGeometry=layerCase==='baseline'?[]:await win.webContents.executeJavaScript("[...document.querySelectorAll('[data-source-node]')].map(e=>({index:Number(e.dataset.sourceNode),bounds:e.getBoundingClientRect().toJSON(),transform:getComputedStyle(e).transform,overflow:getComputedStyle(e).overflow,containerType:getComputedStyle(e).containerType,scrollTop:e.scrollTop}))")
|
||
for(const actual of ancestorGeometry){
|
||
const expected=sourceNodes.find(node=>node.node_index===actual.index)
|
||
assert.deepEqual([actual.bounds.x,actual.bounds.y,actual.bounds.width,actual.bounds.height],expected.bounds,'source ancestor '+actual.index+' bounds')
|
||
if(actual.index===777)assert.equal(actual.transform,expected.styles.transform)
|
||
}
|
||
// A native source-GPU screenshot, at exactly the original REA table coordinates and panel width.
|
||
win.showInactive()
|
||
await new Promise(resolve=>setTimeout(resolve,200))
|
||
win.webContents.debugger.attach('1.3')
|
||
const capture=await Promise.race([win.webContents.debugger.sendCommand('Page.captureScreenshot',{format:'png',fromSurface:true,captureBeyondViewport:false}),new Promise((_,reject)=>setTimeout(()=>reject(Error('Own table screenshot timeout')),8000))])
|
||
const image=Buffer.from(capture.data,'base64');await fs.writeFile(path.join(output,'screen'+suffix+'.png'),image)
|
||
const original=nativeImage.createFromPath(path.resolve('.tmp/rea-assessment-initial/workbuddy.png'))
|
||
const candidate=nativeImage.createFromBuffer(image),roi={x:324,y:385,width:438,height:134}
|
||
const before=original.crop(roi).toBitmap(),after=candidate.crop(roi).toBitmap()
|
||
const textBounds=sourceNodes.filter(node=>node.node_index>=1593&&node.node_index<=1620&&node.node_name==='#text').map(node=>node.bounds)
|
||
let changed=0,changedOutsideText=0,max=0,sum=0;const outsideTextSamples=[]
|
||
for(let i=0;i<before.length;i+=4){let different=false;for(let k=0;k<3;k++){const delta=Math.abs(before[i+k]-after[i+k]);max=Math.max(max,delta);sum+=delta;if(delta)different=true}if(different){changed++;const x=roi.x+(i/4)%roi.width,y=roi.y+Math.floor((i/4)/roi.width);if(!textBounds.some(b=>x>=Math.floor(b[0])-1&&x<Math.ceil(b[0]+b[2])+1&&y>=Math.floor(b[1])-1&&y<Math.ceil(b[1]+b[3])+1)){changedOutsideText++;if(outsideTextSamples.length<8)outsideTextSamples.push({x,y,reference:[...before.subarray(i,i+3)],candidate:[...after.subarray(i,i+3)]})}}}
|
||
await fs.writeFile(path.join(output,'reference-roi.png'),original.crop(roi).toPNG())
|
||
await fs.writeFile(path.join(output,'candidate-roi'+suffix+'.png'),candidate.crop(roi).toPNG())
|
||
win.webContents.sendInputEvent({type:'mouseMove',x:600,y:402})
|
||
await new Promise(resolve=>setTimeout(resolve,180))
|
||
const quote=await win.webContents.executeJavaScript("(()=>{const e=document.querySelector('.cr-message-quote-table-action'),r=e.getBoundingClientRect();return {x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2),opacity:getComputedStyle(e.closest('.cr-message-quote-table-menu')).opacity}})()")
|
||
assert.equal(quote.opacity,'1')
|
||
win.webContents.sendInputEvent({type:'mouseDown',button:'left',clickCount:1,x:quote.x,y:quote.y})
|
||
win.webContents.sendInputEvent({type:'mouseUp',button:'left',clickCount:1,x:quote.x,y:quote.y})
|
||
await new Promise(resolve=>setTimeout(resolve,50))
|
||
assert.equal(await win.webContents.executeJavaScript('window.qa.quoted.length'),1)
|
||
await win.webContents.executeJavaScript('window.qa.mountPath()')
|
||
await new Promise(resolve=>setTimeout(resolve,100))
|
||
const pathPoint=await win.webContents.executeJavaScript("(()=>{const e=document.querySelector('td .cr-clickable-path'),r=e.getBoundingClientRect();return {x:Math.floor(r.x+10),y:Math.floor(r.y+r.height/2)}})()")
|
||
win.webContents.sendInputEvent({type:'mouseMove',...pathPoint})
|
||
win.webContents.sendInputEvent({type:'mouseDown',button:'left',clickCount:1,...pathPoint})
|
||
win.webContents.sendInputEvent({type:'mouseUp',button:'left',clickCount:1,...pathPoint})
|
||
await new Promise(resolve=>setTimeout(resolve,60))
|
||
assert.deepEqual(await win.webContents.executeJavaScript('window.qa.opened'),['C:/fixture/very-long-demo-file-to-check-horizontal-scroll.ts'])
|
||
assert.equal(await win.webContents.executeJavaScript("(()=>{const e=document.querySelector('.cr-table-block__scroller');e.scrollLeft=0;return !e.dispatchEvent(new WheelEvent('wheel',{deltaX:-100,bubbles:true,cancelable:true}))})()"),true,'source horizontal overscroll guard remains active')
|
||
assert.equal(errors.length,0)
|
||
const pixel={roi,pixels:roi.width*roi.height,changed,changedOutsideText,outsideTextSamples,max,meanRgbDelta:sum/(roi.width*roi.height*3),fullPixelParity:changed===0}
|
||
const gpu=await app.getGPUInfo('complete')
|
||
const report={metrics,pixel,gpu,gpuFeatureStatus:app.getGPUFeatureStatus(),errors,layerCase,ancestorGeometry,experiment:experiment?'Own-renderer disable-lcd-text negative control; original startup flag not observed':null,sourceFlow:{originalTransform:'matrix(1, 0, 0, 1, 0, 11220)',fixtureTransform:'translateY(11220px)',note:'Original virtual-flow transform retained with the fixed fixture host offset; preserves transformed ancestor painting without reproducing list virtualization.'},referenceEvidence:'ev_d6e6920ff4e47f73a182fac5312c5f02393366a035445d18c8a6733a8c8a11ed'}
|
||
await fs.writeFile(path.join(output,'report'+suffix+'.json'),JSON.stringify(report,null,2))
|
||
console.log(JSON.stringify({rows:metrics.rows,pixel,errors}))
|
||
win.destroy();app.quit()
|
||
}).catch(error=>{console.error(error);app.exit(1)})
|
||
}
|