88 lines
7.8 KiB
JavaScript
88 lines
7.8 KiB
JavaScript
// Exercise the repaired REA public CLI against an isolated, owned Electron fixture.
|
|
// This proves capture capability, never visual parity with the WorkBuddy application.
|
|
import assert from 'node:assert/strict'
|
|
import { createRequire } from 'node:module'
|
|
import { spawn } from 'node:child_process'
|
|
import { createServer } from 'node:net'
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const root=path.resolve('.tmp/rea-visual-probe-self-test')
|
|
await fs.mkdir(root,{recursive:true})
|
|
const portServer=createServer()
|
|
await new Promise(resolve=>portServer.listen(0,'127.0.0.1',resolve))
|
|
const port=portServer.address().port
|
|
await new Promise(resolve=>portServer.close(resolve))
|
|
const fixture=path.join(root,'index.html'),ready=path.join(root,'ready.json')
|
|
await fs.rm(ready,{force:true})
|
|
await fs.writeFile(fixture,`<!doctype html><meta charset="utf-8"><style>
|
|
body{margin:24px;font:15px/24px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#242424;background:white}
|
|
.cr-agent__name{font-size:12px;font-weight:600}.cr-self-bubble{background:#eee;border-radius:12px;padding:12px}
|
|
code{font:13px/20px Consolas,monospace}button{padding:8px}#panel{height:120px;background:#def;transition:height .2s cubic-bezier(.2,0,0,1);overflow:hidden}#panel.closed{height:0}
|
|
#scroll{height:80px;overflow:auto}#long{height:600px}</style>
|
|
<div class="cr-agent__name">WorkBuddy</div><div class="cr-agent__content"><div class="cr-text-block"><p>字体字形:中文测试</p><p>English glyph parity sample.</p><pre><code>const value = 42;</code></pre></div></div>
|
|
<div class="cr-self-bubble">用户气泡 Mixed 123</div><button id="toggle" onclick="document.querySelector('#panel').classList.toggle('closed')">Toggle</button><div id="panel">Native input animation</div>
|
|
<div id="scroll"><div id="long">Scroll fixture</div></div><input id="editable" value="Never submit" oninput="document.querySelector('#query-output').textContent=this.value"><span class="cr-agent" id="query-output"></span>`)
|
|
const main=path.join(root,'main.cjs')
|
|
await fs.writeFile(main,`const {app,BrowserWindow}=require('electron');const fs=require('node:fs');let w;
|
|
app.setPath('userData',${JSON.stringify(path.join(root,'user-data'))});app.disableHardwareAcceleration();
|
|
app.whenReady().then(async()=>{w=new BrowserWindow({show:false,useContentSize:true,width:1000,height:800,webPreferences:{offscreen:true,contextIsolation:true}});await w.loadFile(${JSON.stringify(fixture)});await w.webContents.executeJavaScript('document.fonts.ready.then(()=>true)');fs.writeFileSync(${JSON.stringify(ready)},JSON.stringify({ready:true}));});`)
|
|
const env={...process.env};delete env.ELECTRON_RUN_AS_NODE
|
|
const child=spawn(require('electron'),[main,`--remote-debugging-port=${port}`],{env,windowsHide:true,stdio:'ignore'})
|
|
async function cli(args){
|
|
return await new Promise((resolve,reject)=>{
|
|
const process=spawn(globalThis.process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',...args,'--json'],{env:{...env,REA_VISUAL_TRACE:'1'},windowsHide:true,stdio:['ignore','pipe','pipe']})
|
|
let out='',err='';process.stdout.on('data',b=>out+=b);process.stderr.on('data',b=>err+=b)
|
|
const timer=setTimeout(()=>{process.kill();reject(new Error('REA fixture CLI timeout'))},45000)
|
|
process.once('error',reject);process.once('close',code=>{clearTimeout(timer);try{const result=JSON.parse(out);if(code||result.error)reject(new Error(JSON.stringify(result)+err));else resolve(result)}catch(error){reject(error)}})
|
|
})
|
|
}
|
|
function normalized(value){return value.normalized_result??value.result?.normalized_result??value.result??value}
|
|
try{
|
|
for(let n=0;n<80;n++){try{await fs.access(ready);break}catch{if(child.exitCode!==null)throw new Error('Owned fixture exited');await delay(100)}}
|
|
await fs.access(ready)
|
|
const endpoint=`http://127.0.0.1:${port}`
|
|
const targets=await cli(['list-electron-targets',endpoint])
|
|
await fs.writeFile(path.join(root,'targets.json'),JSON.stringify(targets,null,2))
|
|
const target=normalized(targets).targets.find(t=>path.resolve(t.file_path)===fixture)
|
|
assert.ok(target,'REA must discover the owned file fixture')
|
|
const probe={include_fonts:true,track_selectors:['#panel'],actions:[
|
|
{kind:'click',step_id:'collapse',selector:'#toggle',restore:true,frames:8,interval_ms:40},
|
|
{kind:'hover',step_id:'hover',selector:'#toggle',frames:1},
|
|
{kind:'viewport',step_id:'resize',width:900,height:700,frames:1}
|
|
]}
|
|
const captured=await cli(['inspect-electron-page',endpoint,target.target_id,'--include-visuals','--visual-probe',JSON.stringify(probe)])
|
|
await fs.writeFile(path.join(root,'capture.json'),JSON.stringify(captured,null,2))
|
|
const result=normalized(captured),visual=result.visuals
|
|
assert.equal(visual.probe.status,'completed')
|
|
assert.deepEqual(visual.font_missing,[])
|
|
assert.equal(new Set(visual.font_samples.map(s=>s.category)).size,5)
|
|
for(const sample of visual.font_samples)assert.ok(sample.platformFonts.some(f=>f.glyphCount>0))
|
|
assert.equal(visual.probe.frames.length,10)
|
|
const collapsing=visual.probe.frames.filter(f=>f.step_id==='collapse')
|
|
const heights=collapsing.map(f=>f.regions[0].bounds?.[3])
|
|
assert.ok(heights.some(h=>h>0),'Must sample an intermediate animation frame')
|
|
assert.equal(heights.at(-1),0)
|
|
assert.ok(collapsing.every((f,i)=>i===0||f.at_ms>collapsing[i-1].at_ms))
|
|
assert.ok(visual.probe.restoration.some(r=>r.kind==='click'&&r.status==='completed'))
|
|
assert.ok(visual.probe.restoration.some(r=>r.kind==='viewport'&&r.status==='completed'))
|
|
for(const [i,frame] of visual.probe.frames.entries())await fs.writeFile(path.join(root,`frame-${String(i).padStart(3,'0')}.png`),Buffer.from(frame.artifact.data_base64,'base64'))
|
|
await delay(300)
|
|
const after=normalized(await cli(['inspect-electron-page',endpoint,target.target_id,'--include-visuals']))
|
|
assert.equal(after.visuals.css_viewport.width,1000)
|
|
const panel=after.visuals.layout.documents[0].nodes.find(n=>n.attributes.id==='panel')
|
|
assert.equal(panel.bounds[3],120)
|
|
const typed=normalized(await cli(['inspect-electron-page',endpoint,target.target_id,'--include-visuals','--visual-probe',JSON.stringify({actions:[{kind:'text',step_id:'type-query',selector:'#editable',text:'腾讯 Tencent',frames:1}]})]))
|
|
assert.equal(typed.visuals.probe.status,'completed')
|
|
const typedAfter=normalized(await cli(['inspect-electron-page',endpoint,target.target_id,'--include-visuals','--visual-probe',JSON.stringify({include_message_content:true})]))
|
|
await fs.writeFile(path.join(root,'text-input-observation.json'),JSON.stringify({typed,typedAfter},null,2))
|
|
assert.ok(typedAfter.visuals.message_content.nodes.some(node=>node.node_name==='#text'&&node.text==='腾讯 Tencent'),'Native text input fires input handler and renders the query')
|
|
const cleared=normalized(await cli(['inspect-electron-page',endpoint,target.target_id,'--include-visuals','--visual-probe',JSON.stringify({actions:[{kind:'text',step_id:'clear-query',selector:'#editable',text:'',frames:1}]})]))
|
|
assert.equal(cleared.visuals.probe.status,'completed')
|
|
await fs.writeFile(path.join(root,'text-input-capture.json'),JSON.stringify({typed,typedAfter,cleared},null,2))
|
|
await fs.writeFile(path.join(root,'verification.json'),JSON.stringify({kind:'ycode-self-test',passed:true,originalApplicationParity:false,fontCategories:visual.font_samples.map(s=>s.category),fonts:visual.font_samples.map(s=>({category:s.category,platformFonts:s.platformFonts})),heights,frameTimes:collapsing.map(f=>f.at_ms),restoration:visual.probe.restoration,after:{width:after.visuals.css_viewport.width,panelHeight:panel.bounds[3]}},null,2))
|
|
console.log('REA live public CLI passed: 5 actual glyph samples, 10 PNG frames, measured collapse motion, toggle and viewport restoration (owned fixture only)')
|
|
}finally{child.kill()}
|