87 lines
11 KiB
JavaScript
87 lines
11 KiB
JavaScript
// Own isolated renderer QA. Original observations come only from the pinned REA archive.
|
|
const fs=require('node:fs/promises'),path=require('node:path'),crypto=require('node:crypto'),assert=require('node:assert/strict')
|
|
const output=path.resolve('.tmp/artifact-link-ui-regression')
|
|
const reference=path.resolve('.tmp/rea-assessment-initial/layout.json')
|
|
const referenceId='ev_d6e6920ff4e47f73a182fac5312c5f02393366a035445d18c8a6733a8c8a11ed'
|
|
const sha=bytes=>crypto.createHash('sha256').update(bytes).digest('hex')
|
|
if(!process.versions.electron){
|
|
;(async()=>{
|
|
await fs.mkdir(output,{recursive:true})
|
|
const entry=`import React,{useState} from 'react';import {createRoot} from 'react-dom/client';
|
|
import SourceArtifactLink from './src/renderer/src/components/SourceArtifactLink';
|
|
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';
|
|
window.qa={clicks:0};function Fixture(){const [count,setCount]=useState(13);window.qa.setCount=setCount;return <div className="chat-pane cr-theme" data-cr-theme="light" style={{position:'absolute',inset:0,display:'block',overflow:'visible',fontSize:13,lineHeight:'19px'}}>
|
|
<div className="conversation-finished-footer" style={{position:'absolute',left:324.28125,top:602.578125,width:436.703125}}><SourceArtifactLink count={count} onClick={()=>window.qa.clicks++}/></div>
|
|
</div>}createRoot(document.getElementById('root')).render(<Fixture/>);`
|
|
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'})
|
|
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:30000})
|
|
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')
|
|
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms))
|
|
app.whenReady().then(async()=>{
|
|
const source=JSON.parse(await fs.readFile(reference,'utf8'))
|
|
assert.equal(source.evidenceId,referenceId)
|
|
const sourceNodes=source.layout.documents[0].nodes,sourceSvg=source.layout.documents[0].svg_nodes
|
|
const errors=[],checks=[],win=new BrowserWindow({x:0,y:0,width:1336,height:901,useContentSize:true,frame:false,show:false,webPreferences:{contextIsolation:true,nodeIntegration:false,backgroundThrottling:false}})
|
|
win.webContents.on('console-message',event=>{if(event.level==='error')errors.push(event.message)})
|
|
const check=(label,actual,expected)=>checks.push({label,pass:JSON.stringify(actual)===JSON.stringify(expected),actual,expected})
|
|
const read=expression=>win.webContents.executeJavaScript(expression)
|
|
await win.loadFile(path.join(output,'index.html'));await delay(250)
|
|
win.showInactive();win.webContents.sendInputEvent({type:'mouseMove',x:0,y:0});await delay(200)
|
|
const metrics=await read(`(()=>{const selectors={actions:'.artifact-slot-panel__actions',button:'.artifact-slot-panel__action-btn',chevron:'.artifact-slot-panel__action-chevron',svg:'.artifact-slot-panel__action-chevron svg'};const properties=${JSON.stringify(source.layout.properties)};return {nodes:Object.fromEntries(Object.entries(selectors).map(([key,selector])=>{const e=document.querySelector(selector),r=e.getBoundingClientRect(),c=getComputedStyle(e);return[key,{bounds:[r.x,r.y,r.width,r.height],styles:Object.fromEntries(properties.map(name=>[name,c.getPropertyValue(name)])),attributes:Object.fromEntries([...e.attributes].map(a=>[a.name,a.value]))}]})),slotDisplay:getComputedStyle(document.querySelector('[data-chat-search-skip]')).display,text:document.querySelector('.artifact-slot-panel__action-btn').textContent}})()`)
|
|
for(const [name,index] of [['actions',1644],['button',1645],['chevron',1647],['svg',1648]]){
|
|
const expected=sourceNodes.find(node=>node.node_index===index)
|
|
check(name+' bounds',metrics.nodes[name].bounds,expected.bounds)
|
|
for(const [property,value] of Object.entries(metrics.nodes[name].styles))check(name+' '+property,value,expected.styles[property])
|
|
}
|
|
check('display-contents slot',metrics.slotDisplay,'contents')
|
|
check('exact count label',metrics.text,'查看所有产物 (13)')
|
|
check('single direct label text node',await read("[...document.querySelector('.artifact-slot-panel__action-btn').childNodes].filter(n=>n.nodeType===Node.TEXT_NODE).map(n=>n.textContent)"),['查看所有产物 (13)'])
|
|
check('SVG attributes',Object.fromEntries(['width','height','viewBox','fill'].map(key=>[key,metrics.nodes.svg.attributes[key]])),sourceSvg.find(node=>node.node_index===1648).attributes)
|
|
check('SVG path',await read("document.querySelector('.artifact-slot-panel__action-chevron path').getAttribute('d')"),sourceSvg.find(node=>node.node_index===1649).attributes.d)
|
|
win.webContents.debugger.attach('1.3')
|
|
const capture=await Promise.race([win.webContents.debugger.sendCommand('Page.captureScreenshot',{format:'png',fromSurface:true,captureBeyondViewport:false}),delay(5000).then(()=>{throw Error('Own artifact-link screenshot timeout')})])
|
|
const screenshot=Buffer.from(capture.data,'base64');await fs.writeFile(path.join(output,'screen.png'),screenshot)
|
|
const originalBytes=await fs.readFile('.tmp/rea-assessment-initial/workbuddy.png')
|
|
assert.equal(sha(originalBytes),source.artifact.sha256,'reference PNG must match retained REA artifact')
|
|
const original=nativeImage.createFromBuffer(originalBytes),candidate=nativeImage.createFromBuffer(screenshot),roi={x:324,y:614,width:139,height:26}
|
|
const a=original.crop(roi),b=candidate.crop(roi),before=a.toBitmap(),after=b.toBitmap()
|
|
await fs.writeFile(path.join(output,'reference-roi.png'),a.toPNG());await fs.writeFile(path.join(output,'candidate-roi.png'),b.toPNG())
|
|
let changed=0,max=0,sum=0,changedInSvg=0;const deltas=[]
|
|
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]);sum+=delta;max=Math.max(max,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(x>=450&&x<459&&y>=621&&y<633)changedInSvg++;if(deltas.length<12)deltas.push({x,y,reference:[...before.subarray(i,i+3)],candidate:[...after.subarray(i,i+3)]})}}
|
|
const pixel={roi,pixels:roi.width*roi.height,changed,changedInSvg,max,meanRgbDelta:sum/(roi.width*roi.height*3),fullPixelParity:changed===0,deltas,referenceSha256:sha(a.toPNG()),candidateSha256:sha(b.toPNG())}
|
|
const [x,y,w,h]=metrics.nodes.button.bounds,point={x:Math.floor(x+w/2),y:Math.floor(y+h/2)}
|
|
win.webContents.sendInputEvent({type:'mouseMove',...point});await delay(200)
|
|
const hover=await read("(()=>{const b=document.querySelector('.artifact-slot-panel__action-btn'),c=document.querySelector('.artifact-slot-panel__action-chevron');return{buttonColor:getComputedStyle(b).color,chevronColor:getComputedStyle(c).color,bounds:b.getBoundingClientRect().toJSON()}})()")
|
|
check('own hover button color',hover.buttonColor,'rgb(0, 0, 0)');check('own hover chevron color',hover.chevronColor,'rgb(0, 0, 0)')
|
|
win.webContents.sendInputEvent({type:'mouseDown',button:'left',clickCount:1,...point});win.webContents.sendInputEvent({type:'mouseUp',button:'left',clickCount:1,...point});await delay(50)
|
|
check('native click callback once',await read('window.qa.clicks'),1)
|
|
win.focus();win.webContents.focus();await delay(100)
|
|
const keyboard={windowFocused:win.isFocused(),webContentsFocused:win.webContents.isFocused(),buttonFocused:await read("document.activeElement?.matches('.artifact-slot-panel__action-btn')===true"),tested:false}
|
|
if(keyboard.windowFocused&&keyboard.webContentsFocused&&keyboard.buttonFocused){
|
|
win.webContents.sendInputEvent({type:'keyDown',keyCode:'Return'});win.webContents.sendInputEvent({type:'char',keyCode:'\r'});win.webContents.sendInputEvent({type:'keyUp',keyCode:'Return'});await delay(50)
|
|
keyboard.tested=true
|
|
check('native Enter callback once',await read('window.qa.clicks'),2)
|
|
}
|
|
await read('window.qa.setCount(0)');await delay(50)
|
|
check('empty count omits entry',await read("document.querySelector('.artifact-slot-panel__action-btn')===null"),true)
|
|
check('renderer errors',errors,[])
|
|
const integration=JSON.parse(await fs.readFile('.tmp/panel-integration-Capd62/report.json','utf8'))
|
|
const gpuFeatureStatus=app.getGPUFeatureStatus(),gpu=await app.getGPUInfo('complete')
|
|
check('archived GPU feature status',gpuFeatureStatus,source.environment_gpu.feature_status)
|
|
const report={result:checks.every(check=>check.pass)?'PASS geometry/styles/own interactions':'FAIL geometry/styles/own interactions',checks,counts:{total:checks.length,pass:checks.filter(check=>check.pass).length,fail:checks.filter(check=>!check.pass).length},metrics,pixel,hover,keyboard,reference:{evidenceId:referenceId,archive:source.archive,screenshotSha256:sha(originalBytes),version:'WorkBuddy 5.8.2'},gpuFeatureStatus,sourceGpuFeatureStatus:source.environment_gpu.feature_status,gpu,productionHashes:{component:sha(await fs.readFile('src/renderer/src/components/SourceArtifactLink.tsx')),css:sha(await fs.readFile('src/renderer/src/components/source-artifact-link.css'))},integration:{result:integration.result,total:integration.checks.length,pass:integration.checks.filter(check=>check.pass).length,scope:integration.scope},limitations:['Original hover state is not archived; own hover and activation tests do not establish original hover pixel parity.','The callback is an isolated counter; actual overview/tab retention is covered by the separate production integration report.',...(!keyboard.tested?['Native Enter was not tested because a real focused window/webContents/button precondition was unavailable.']:[]),'No production GPU flags or UI source changed; no WorkBuddy runtime actions or new original evidence were collected.']}
|
|
await fs.writeFile(path.join(output,'report.json'),JSON.stringify(report,null,2))
|
|
console.log(JSON.stringify({result:report.result,checks:report.counts,button:metrics.nodes.button.bounds,svg:metrics.nodes.svg.bounds,pixel,integration:report.integration.total}))
|
|
win.destroy();app.exit(report.counts.fail?1:0)
|
|
}).catch(error=>{console.error(error);app.exit(1)})
|
|
}
|