Files
ycode/scripts/rea/test-composer-submenus-native.mjs

48 lines
15 KiB
JavaScript

/** Current cr submenu bodies with controlled own host data; no original UI access. */
import fs from 'node:fs'
import path from 'node:path'
import assert from 'node:assert/strict'
import{createRequire}from'node:module'
import{spawn}from'node:child_process'
const require=createRequire(import.meta.url),module=require('electron'),electron=typeof module==='object'&&module.app?module:null
if(electron){electron.app.disableHardwareAcceleration();electron.app.commandLine.appendSwitch('use-angle','swiftshader');electron.app.commandLine.appendSwitch('disable-gpu-compositing')}
const root=path.resolve('.'),base=path.join(root,'.tmp')
if(!electron){const output=fs.mkdtempSync(path.join(base,'composer-submenus-native-')),env={...process.env};delete env.ELECTRON_RUN_AS_NODE;delete env.ELECTRON_RENDERER_URL;const child=spawn(module,['--no-sandbox','--disable-gpu-sandbox',path.resolve('scripts/rea/test-composer-submenus-native.mjs'),output],{env,windowsHide:true,stdio:'inherit'});process.exitCode=await new Promise(resolve=>child.on('exit',code=>resolve(code??1)))}
else void(async()=>{
const{app,BrowserWindow}=electron,output=path.resolve(process.argv.at(-1));assert.ok(path.relative(base,output).startsWith('composer-submenus-native-')&&!path.relative(base,output).includes('..'));fs.mkdirSync(path.join(output,'data'));app.setPath('userData',path.join(output,'data'));await app.whenReady()
const location=file=>JSON.stringify(path.join(root,file).replaceAll('\\','/'))
fs.writeFileSync(path.join(output,'fixture.tsx'),`import React,{useState}from'react';import{createRoot}from'react-dom/client';import{SourceFilesSubmenu,SourceModeSubmenu,SourceConnectorsSubmenu,SourceReferenceFilesSubmenu}from ${location('src/renderer/src/workbuddy/composer/add-menu/SourceSubmenus')};import ${location('src/renderer/src/styles/tokens.css')};import ${location('src/renderer/src/styles/app.css')};
window.qa={local:0,close:0,modes:[],connect:[],toggle:[],cancel:[],repair:[],manage:0,references:[]};const items=[{id:'new',name:'New connector'},{id:'connected',name:'Connected connector',isConnected:true,enabled:true},{id:'busy',name:'Connecting connector',status:'connecting',isCancellable:true},{id:'disabled',name:'Disabled connector',canToggle:false,disabledReason:'CONTROLLED_DISABLED'},{id:'error',name:'Error connector',isConnected:true,enabled:true,errorText:'CONTROLLED_CONNECTION_ERROR',errorKind:'retry'}];function App(){const[tab,setTab]=useState('files'),[disabled,setDisabled]=useState(false),[selected,setSelected]=useState('craft');Object.assign(window.qa,{setTab,setDisabled,setSelected});const close=()=>window.qa.close++;return<div className="yc-source-composer-add-menu cr-theme" data-cr-theme="light" style={{position:'absolute',left:100,top:100}}>{tab==='files'?<SourceFilesSubmenu provider={{onPickLocalFiles:()=>window.qa.local++}} onCloseMenu={close}/>:tab==='mode'?<SourceModeSubmenu selectedId={selected} provider={{disabled,onChange:mode=>window.qa.modes.push(mode)}}/>:tab==='connectors'?<SourceConnectorsSubmenu provider={{items,onConnectItem:item=>window.qa.connect.push(item.id),onToggleItem:(item,on)=>window.qa.toggle.push([item.id,on]),onCancelConnect:item=>window.qa.cancel.push(item.id),onRepairItem:item=>window.qa.repair.push(item.id),onSummonMore:()=>window.qa.manage++}} onCloseMenu={close}/>:tab==='references'?<SourceReferenceFilesSubmenu provider={{items:[{id:'file',name:'report.final.md',path:'C:/controlled/report.final.md'},{id:'folder',name:'folder',path:'C:/controlled/folder',isDirectory:true}],onSelect:item=>window.qa.references.push(item)}} onCloseMenu={close}/>:<SourceFilesSubmenu provider={{}}/>}</div>}createRoot(document.getElementById('root')).render(<App/>);`)
await require('esbuild').build({entryPoints:[path.join(output,'fixture.tsx')],bundle:true,platform:'browser',format:'iife',jsx:'automatic',outfile:path.join(output,'fixture.js'),logLevel:'warning'})
fs.writeFileSync(path.join(output,'index.html'),'<html data-theme="light"><head><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"><style>html,body,#root{margin:0;width:100%;height:100%}</style></head><body><div id="root"></div><script src="fixture.js"></script></body></html>')
const win=new BrowserWindow({width:900,height:680,frame:false,useContentSize:true,show:false,webPreferences:{contextIsolation:true,nodeIntegration:false,backgroundThrottling:false}}),checks=[],errors=[],measurements={}
win.webContents.on('console-message',event=>{if(event.level==='error')errors.push(event.message)});await win.loadFile(path.join(output,'index.html'));win.webContents.debugger.attach('1.3');await win.webContents.debugger.sendCommand('Page.enable');win.showInactive()
const read=code=>win.webContents.executeJavaScript(code),cdp=(method,args)=>win.webContents.debugger.sendCommand(method,args),pause=ms=>new Promise(resolve=>setTimeout(resolve,ms)),check=(name,actual,expected)=>{assert.deepEqual(actual,expected,name);checks.push(name)}
const wait=async code=>{for(let i=0;i<100;i++){if(await read(code))return;await pause(30)}throw Error('Timeout '+code)}
const bounds=selector=>read(`(()=>{const r=document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect();return[r.x,r.y,r.width,r.height]})()`)
const click=async selector=>{const r=await bounds(selector);await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',x:r[0]+r[2]/2,y:r[1]+r[3]/2});for(const type of ['mousePressed','mouseReleased'])await cdp('Input.dispatchMouseEvent',{type,x:r[0]+r[2]/2,y:r[1]+r[3]/2,button:'left',clickCount:1});await pause(100)}
const update=async code=>{await read(code);await pause(100)},capture=async name=>fs.writeFileSync(path.join(output,name+'.png'),Buffer.from((await cdp('Page.captureScreenshot',{format:'png',fromSurface:true})).data,'base64'))
const selectTab=async name=>{await update(`window.qa.setTab(${JSON.stringify(name)})`);await pause(100)}
try{
await wait('!!document.querySelector(".cr-files-submenu")');measurements.files=await bounds('.cr-files-submenu');check('source files width200',measurements.files[2],200);check('only actual local file handler shown',await read('[...document.querySelectorAll(".cr-files-submenu__item-label")].map(e=>e.textContent)'),['本地文件']);check('source upload SVG recovered',await read('!!document.querySelector(".cr-files-submenu__item-icon svg path")'),true);await capture('files');await click('.cr-files-submenu__item');check('local file callback exactly once',await read('window.qa.local'),1);check('file selection closes via host',await read('window.qa.close'),1)
await selectTab('mode');await wait('document.querySelectorAll(".wb-switch__input").length===2');measurements.mode=await bounds('.cr-mode-submenu');check('source mode width200',measurements.mode[2],200);check('original plan/ask two options',await read('[...document.querySelectorAll(".cr-mode-submenu__row-en")].map(e=>e.textContent)'),['Plan','Ask'])
// Independent iframe applies the unmodified retained original CSS. It cannot mask a scoped-recovery selector bug in the actual component.
const rawCss=fs.readFileSync('.tmp/rea-css-recovered/src-CBaV_A3S.css','utf8')+'\n'+fs.readFileSync('.tmp/rea-css-recovered/src-Bh3YZ35H.css','utf8')
const originalHtml='<html data-theme="light"><head><style>'+rawCss+'</style></head><body><label class="wb-switch wb-switch--medium"><span class="wb-switch__control"><input type="checkbox" class="wb-switch__input"><span class="wb-switch__track"><span class="wb-switch__thumb"></span></span></span></label><button class="wb-button wb-button--ghost wb-button--medium">Reference</button></body></html>'
await read(`(()=>{const frame=document.createElement('iframe');frame.id='original-css-reference';frame.style.cssText='position:absolute;left:-10000px;width:400px;height:200px';frame.srcdoc=${JSON.stringify(originalHtml)};document.body.append(frame);const probe=document.createElement('button');probe.id='scoped-theme-button';probe.className='wb-button wb-button--ghost wb-button--medium';probe.textContent='Probe';document.querySelector('.yc-source-composer-add-menu').append(probe)})()`)
await wait('!!document.querySelector("#original-css-reference").contentDocument.querySelector(".wb-switch__track")')
const themePair=()=>read(`(()=>{const frame=document.querySelector('#original-css-reference').contentDocument,own=document.querySelector('.yc-source-composer-add-menu');const measure=(doc,scope)=>({track:doc.defaultView.getComputedStyle(doc.querySelector('.wb-switch__track')).backgroundColor,trackToken:doc.defaultView.getComputedStyle(scope).getPropertyValue('--wb-switch-track-bg').trim(),gray5:doc.defaultView.getComputedStyle(scope).getPropertyValue('--wb-palette-gray-5').trim(),buttonColor:doc.defaultView.getComputedStyle(doc.querySelector('.wb-button')).color,buttonHoverToken:doc.defaultView.getComputedStyle(scope).getPropertyValue('--wb-button-ghost-bg-hover').trim()});return{actual:measure(document,own),original:measure(frame,frame.documentElement)}})()`)
await pause(250);measurements.light=await themePair();check('light off actual source token RGB230',measurements.light.actual.track,'rgb(230, 230, 230)');check('light switch and button match unmodified original CSS',measurements.light.actual,measurements.light.original);await capture('mode-light-off')
await read(`document.documentElement.dataset.theme='dark';document.querySelector('.yc-source-composer-add-menu').dataset.crTheme='dark';document.querySelector('#original-css-reference').contentDocument.documentElement.dataset.theme='dark'`);await pause(250);measurements.dark=await themePair();check('dark switch and button match unmodified original CSS',measurements.dark.actual,measurements.dark.original);check('dark off stays distinct from light',measurements.dark.actual.track!==measurements.light.actual.track,true);await capture('mode-dark-off')
await read(`document.documentElement.dataset.theme='light';document.querySelector('.yc-source-composer-add-menu').dataset.crTheme='light';document.querySelector('#original-css-reference').contentDocument.documentElement.dataset.theme='light'`);await pause(250);check('returning light removes dark token effects',(await themePair()).actual,measurements.light.original)
await click('.cr-mode-submenu__row:first-child .wb-switch__control');check('source plan toggles on via actual callback',await read('window.qa.modes.at(-1).id'),'plan');await click('.cr-mode-submenu__row:first-child .wb-switch__control');check('second plan click toggles back to defaultcraft',await read('window.qa.modes.at(-1).id'),'craft');await update('window.qa.setDisabled(true)');check('host disabled controls both inputs',await read('[...document.querySelectorAll(".wb-switch__input")].every(e=>e.disabled)'),true);await capture('mode');await update('window.qa.setDisabled(false)')
await selectTab('connectors');await wait('!!document.querySelector(".cr-connectors-submenu input")');measurements.connectors=await bounds('.cr-connectors-submenu');check('source connectors width240',measurements.connectors[2],240);check('source configured items lead list',await read('[...document.querySelectorAll(".cr-connectors-submenu__item-name")].map(e=>e.textContent)'),['Connected connector','Error connector','New connector','Connecting connector','Disabled connector']);await capture('connectors')
await click('.cr-connectors-submenu__row-tooltip:nth-child(1) .cr-connectors-submenu__item');check('connected source row toggles actual host off',await read('window.qa.toggle'),[['connected',false]]);await click('.cr-connectors-submenu__row-tooltip:nth-child(3) .cr-connectors-submenu__item');check('new source row invokes actual connect host',await read('window.qa.connect'),['new']);await click('.cr-connectors-submenu__row-tooltip:nth-child(4) .cr-connectors-submenu__item');check('connecting source cancellable row invokes cancel once',await read('window.qa.cancel'),['busy']);await click('.cr-connectors-submenu__row-tooltip:nth-child(5) .cr-connectors-submenu__item');check('disabled source row never connects/toggles',await read('[window.qa.connect.length,window.qa.toggle.length]'),[1,1]);const errorBounds=await bounds('.cr-connectors-submenu__row-tooltip:nth-child(2)');await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',x:errorBounds[0]+errorBounds[2]/2,y:errorBounds[1]+errorBounds[3]/2});await pause(200);await click('.cr-connectors-submenu__error-action');check('error repair uses actual host callback',await read('window.qa.repair'),['error'])
await click('.cr-connectors-submenu input');await cdp('Input.insertText',{text:'new'});await pause(250);check('source local connector search',await read('[...document.querySelectorAll(".cr-connectors-submenu__item-name")].map(e=>e.textContent)'),['New connector']);await click('[aria-label="清除搜索"]');check('source clear restores items',await read('document.querySelectorAll(".cr-connectors-submenu__item").length'),5)
await read('document.querySelector(".cr-connectors-submenu input").dispatchEvent(new CompositionEvent("compositionstart",{bubbles:true}))');await cdp('Input.insertText',{text:'missing'});check('IME composing retains source previous results',await read('document.querySelectorAll(".cr-connectors-submenu__item").length'),5);await read('document.querySelector(".cr-connectors-submenu input").dispatchEvent(new CompositionEvent("compositionend",{bubbles:true,data:"missing"}))');await pause(100);check('IME commit shows honest empty result',await read('document.querySelectorAll(".cr-connectors-submenu__item").length'),0);check('empty result source text',await read('document.querySelector(".cr-connectors-submenu__empty").textContent'),'未找到连接器');await click('[aria-label="清除搜索"]');await click('.cr-connectors-submenu__footer');check('manage callback and close exactly once',await read('[window.qa.manage,window.qa.close]'),[1,2])
await selectTab('references');measurements.references=await bounds('.cr-reference-files-submenu');check('source references width240',measurements.references[2],240);check('source filename preserves base/ext split',await read('[document.querySelector(".cr-reference-files-submenu__name-base").textContent,document.querySelector(".cr-reference-files-submenu__name-ext").textContent]'),['report.final','.md']);check('source reference icon host mapping renders both',await read('document.querySelectorAll(".cr-reference-files-submenu__item-icon svg").length'),2);await capture('references');await click('.cr-reference-files-submenu__item');check('source selected actual reference identity/path retained',await read('[window.qa.references[0].id,window.qa.references[0].path]'),['file','C:/controlled/report.final.md']);check('reference selection closes once',await read('window.qa.close'),3)
await selectTab('empty');check('no file handler means no fabricated source rows',await read('!!document.querySelector(".cr-files-submenu")'),false);check('no renderer exceptions',errors,[])
fs.writeFileSync(path.join(output,'report.json'),JSON.stringify({result:'PASS',checks,measurements,scope:'Source rendering/event behavior with controlled own data; no MCP service/file dialog/model execution claim'},null,2));console.log(JSON.stringify({result:'PASS',checks:checks.length,output}));win.destroy();app.exit(0)
}catch(error){await capture('failure');fs.writeFileSync(path.join(output,'failure.json'),JSON.stringify({error:String(error),checks,errors,html:await read('document.body.innerHTML')},null,2));console.error(error);win.destroy();app.exit(1)}
})().catch(error=>{console.error(error);electron.app.exit(1)})