/** Own Electron fixture for the recovered cr model selector; no original runtime 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,'model-selector-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-model-selector-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('model-selector-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 Selector from ${location('src/renderer/src/workbuddy/composer/model-selector/SourceModelSelector')};import ${location('src/renderer/src/styles/tokens.css')};import ${location('src/renderer/src/styles/app.css')};import ${location('src/renderer/src/workbuddy/conversation/source-styles.css')};
window.qa={picks:[],manages:0,levels:[]};
const models=[{id:'ui-model-one',name:'studio-mini-one',providerId:'tencent-token-plan',model:'studio-mini-one',hasApiKey:true,keyStorage:'encrypted',supportsReasoning:true,canDisableThinking:true,supportedEfforts:['low','medium','high','xhigh','max']},{id:'ui-model-two',name:'hunyuan-2.0-thinking',providerId:'tencent-token-plan',model:'hunyuan-2.0-thinking',hasApiKey:true,keyStorage:'encrypted',supportsReasoning:true,canDisableThinking:true,supportedEfforts:['low','medium','high','xhigh','max']},{id:'ui-model-three',name:'gemini-2.5-flash',providerId:'gemini',model:'gemini-2.5-flash',hasApiKey:true,keyStorage:'encrypted'},{id:'ui-model-four',name:'gpt-6-luna',providerId:'custom',model:'gpt-6-luna',hasApiKey:true,keyStorage:'encrypted',supportsReasoning:true,canDisableThinking:true,defaultEffort:'',supportedEfforts:['max']}];
function App(){const[selected,setSelected]=useState('ui-model-two'),[disabled,setDisabled]=useState(false),[dark,setDark]=useState(false),[level,setLevel]=useState('medium');Object.assign(window.qa,{setSelected,setDisabled,setDark,setLevel});return
{window.qa.picks.push(id);setSelected(id)}} onThinkingLevelChange={async level=>{window.qa.levels.push(level);setLevel(level)}} onManage={()=>{window.qa.manages++}}/>
}
createRoot(document.getElementById('root')).render( );`)
await require('esbuild').build({entryPoints:[path.join(output,'fixture.tsx')],bundle:true,platform:'browser',format:'iife',jsx:'automatic',outfile:path.join(output,'fixture.js'),loader:{'.png':'dataurl','.svg':'dataurl'},logLevel:'warning'})
fs.writeFileSync(path.join(output,'index.html'),'
')
const win=new BrowserWindow({width:1336,height:901,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<120;i++){if(await read(code))return;await pause(30)}throw Error('Timeout '+code)}
const bounds=selector=>read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)return null;const r=e.getBoundingClientRect();return[r.x,r.y,r.width,r.height]})()`)
const visualSnapshot=()=>read(`(()=>{const properties=['display','position','box-sizing','font-family','font-size','font-weight','line-height','letter-spacing','color','background-color','border-radius','border-top-width','border-top-color','padding-top','padding-right','padding-bottom','padding-left','margin-top','margin-right','margin-bottom','margin-left','gap','align-items','justify-content','box-shadow','overflow','opacity','visibility','transform','max-height','transition-property','transition-duration','transition-timing-function','text-rendering','-webkit-font-smoothing'];const selectors=['.source-model-selector','.cr-model-selector','.cr-model-selector__trigger','.cr-model-selector__trigger-icon','.cr-model-selector__trigger-label','.cr-model-selector__trigger-arrow','.cr-model-selector__popover','.cr-model-selector__menu','.cr-slim-scroll','.cr-model-selector__list','.cr-model-selector__item','.cr-model-selector__item-icon','.cr-model-selector__item-icon svg','.cr-model-selector__item-check','.cr-model-selector__item-check svg','.cr-model-selector__item-name','.cr-menu-item','.cr-menu-item__icon','.cr-menu-item__icon svg','.cr-menu-item__content'];const nodes=selectors.flatMap(selector=>[...document.querySelectorAll(selector)].map((element,index)=>{const rect=element.getBoundingClientRect(),style=getComputedStyle(element),range=document.createRange();range.selectNodeContents(element);const textRect=range.getBoundingClientRect();const isText=['.cr-model-selector__trigger-label','.cr-model-selector__item-name','.cr-menu-item__content'].some(s=>element.matches(s));return{selector,index,tag:element.tagName,classes:element.className?.baseVal??element.className??'',role:element.getAttribute('role'),inlineStyle:element.matches('.cr-model-selector__popover')?element.getAttribute('style'):undefined,text:isText?element.textContent.trim():undefined,bounds:[rect.x,rect.y,rect.width,rect.height],textBounds:isText?[textRect.x,textRect.y,textRect.width,textRect.height]:undefined,styles:Object.fromEntries(properties.map(property=>[property,style.getPropertyValue(property)]))}}));return{viewport:{width:innerWidth,height:innerHeight,devicePixelRatio},nodes}})()`)
const click=async selector=>{const r=await bounds(selector);assert.ok(r,selector+' present');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(220)}
const hoverSelector=async selector=>{const b=await bounds(selector);assert.ok(b,selector+' present');await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',x:b[0]+b[2]/2,y:b[1]+b[3]/2});await pause(340)}
const hoverRow=async()=>{const b=await bounds('.cr-model-selector__menu .cr-model-selector__item--selected');await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',x:5,y:5});await pause(80);await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',x:b[0]+b[2]/2,y:b[1]+b[3]/2});await pause(340)}
const clickText=async(selector,text)=>{const point=await read(`(()=>{const e=[...document.querySelectorAll(${JSON.stringify(selector)})].find(n=>n.textContent.trim()===${JSON.stringify(text)});if(!e)return null;const b=e.getBoundingClientRect();return[b.x+b.width/2,b.y+b.height/2]})()`);assert.ok(point,'text target '+text);for(const type of['mousePressed','mouseReleased'])await cdp('Input.dispatchMouseEvent',{type,x:point[0],y:point[1],button:'left',clickCount:1});await pause(280)}
const openSubMenu=async()=>{await click('.cr-model-selector__trigger');await wait('!!document.querySelector(".cr-model-selector__menu")');await hoverRow();await wait('!!document.querySelector(".cr-model-selector__sub-menu")');await hoverSelector('.cr-model-selector__sub-menu-action');await wait('!!document.querySelector(".cr-model-selector__sub-panel")')}
const closePopover=async()=>{await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',x:5,y:5});await wait('!document.querySelector(".cr-model-selector__sub-panel")');await click('.cr-model-selector__trigger');await wait('!document.querySelector(".cr-model-selector__menu")')}
const sendKey=async key=>{const code={Escape:27,Enter:13,ArrowDown:40,ArrowUp:38}[key]??0;await cdp('Input.dispatchKeyEvent',{type:'keyDown',key,code:key,windowsVirtualKeyCode:code,...key==='Enter'?{text:'\r'}:{...key==='Escape'?{text:'\u001b'}:{}}});await cdp('Input.dispatchKeyEvent',{type:'keyUp',key,code:key,windowsVirtualKeyCode:code});await pause(320)}
const capture=async name=>fs.writeFileSync(path.join(output,name+'.png'),Buffer.from((await cdp('Page.captureScreenshot',{format:'png',fromSurface:true})).data,'base64'))
try{
await wait('!!document.querySelector(".cr-model-selector__trigger")')
measurements.trigger=await bounds('.cr-model-selector__trigger')
check('source trigger height 32',measurements.trigger[3],32)
check('source trigger pill radius 999px',await read('getComputedStyle(document.querySelector(".cr-model-selector__trigger")).borderRadius'),'999px')
check('source trigger font declaration',await read('getComputedStyle(document.querySelector(".cr-model-selector__trigger")).fontFamily'),'Arial')
check('source trigger font size 13',await read('getComputedStyle(document.querySelector(".cr-model-selector__trigger")).fontSize'),'13px')
check('source trigger padding 8',await read('getComputedStyle(document.querySelector(".cr-model-selector__trigger")).paddingLeft'),'8px')
check('source trigger gap 2',await read('getComputedStyle(document.querySelector(".cr-model-selector__trigger")).gap'),'2px')
check('source trigger aria combobox',await read('[document.querySelector(".cr-model-selector__trigger").getAttribute("role"),document.querySelector(".cr-model-selector__trigger").getAttribute("aria-expanded")]'),['combobox','false'])
measurements.icon=await bounds('.cr-model-selector__trigger-icon');check('source trigger icon 16',measurements.icon.slice(2),[16,16])
measurements.arrow=await bounds('.source-model-selector .wb-icon.cr-model-selector__trigger-arrow');check('source trigger arrow 12',measurements.arrow.slice(2),[12,12])
await capture('closed')
await click('.cr-model-selector__trigger')
await wait('!!document.querySelector(".cr-model-selector__menu")')
measurements.menu=await bounds('.cr-model-selector__menu')
check('source menu width 340',measurements.menu[2],340)
check('composer menu aligns its end with the source trigger',Math.abs((measurements.menu[0]+measurements.menu[2])-(measurements.trigger[0]+measurements.trigger[2]))<=1,true)
check('source menu padding 8',await read('getComputedStyle(document.querySelector(".cr-model-selector__menu")).paddingLeft'),'8px')
check('source menu radius 16',await read('getComputedStyle(document.querySelector(".cr-model-selector__menu")).borderRadius'),'16px')
check('source popover offset 6 within sub-pixel rounding',Math.abs((measurements.trigger[1]-6)-(measurements.menu[1]+measurements.menu[3]))<=1,true)
const rows=await read('[...document.querySelectorAll(".cr-model-selector__menu .cr-model-selector__item")].map(e=>{const r=e.getBoundingClientRect();return[r.height,e.textContent]})')
check('one row per YCode model',rows.length,4)
check('source row height 32',rows.map(r=>r[0]),[32,32,32,32])
check('selected row marked',await read('document.querySelectorAll(".cr-model-selector__menu .cr-model-selector__item--selected").length'),1)
check('selected row carries the current name',await read('document.querySelector(".cr-model-selector__menu .cr-model-selector__item--selected").textContent.includes("hunyuan-2.0-thinking")'),true)
check('config footer present',await read('document.querySelectorAll(".cr-model-selector__menu > .cr-menu-item").length'),1)
check('config footer label matches source catalog',await read('document.querySelector(".cr-model-selector__menu > .cr-menu-item").textContent'),'配置自定义模型')
check('no group submenu rows for a flat registry',await read('document.querySelectorAll(".cr-model-selector__group-entry").length'),0)
check('footer icon box 16x16 from source',await read('(()=>{const e=document.querySelector(".cr-model-selector__menu > .cr-menu-item .cr-menu-item__icon");if(!e)return null;const r=e.getBoundingClientRect();return[r.width,r.height]})()'),[16,16])
check('footer icon right margin 8 from source',await read('getComputedStyle(document.querySelector(".cr-model-selector__menu > .cr-menu-item .cr-menu-item__icon")).marginRight'),'8px')
check('footer label is the flex content cell',await read('!!document.querySelector(".cr-model-selector__menu > .cr-menu-item .cr-menu-item__content")'),true)
check('menu scroll region present',await read('!!document.querySelector(".cr-model-selector__list")'),true)
measurements.openState=await visualSnapshot()
check('open-state fixture viewport matches original capture width',measurements.openState.viewport.width,1336)
check('open-state fixture viewport matches original capture height',measurements.openState.viewport.height,901)
check('open-state fixture uses 1x device scale',measurements.openState.viewport.devicePixelRatio,1)
const measuredNode=(selector,index=0)=>measurements.openState.nodes.find(node=>node.selector===selector&&node.index===index)
check('popover container measured',!!measuredNode('.cr-model-selector'),true)
check('floating popover container stays at source max height',[
measuredNode('.cr-model-selector__popover').role,
measuredNode('.cr-model-selector__popover').styles.position,
measuredNode('.cr-model-selector__popover').styles['max-height'],
measuredNode('.cr-model-selector__popover').bounds.slice(2)
],['listbox','fixed','368px',[340,193]])
check('menu uses border-box sizing',measuredNode('.cr-model-selector__menu').styles['box-sizing'],'border-box')
check('menu border resolves to one device pixel at 1x',measuredNode('.cr-model-selector__menu').styles['border-top-width'],'1px')
check('menu surface uses the recovered light token',measuredNode('.cr-model-selector__menu').styles['background-color'],'rgb(255, 255, 255)')
check('menu edge uses the recovered border token',measuredNode('.cr-model-selector__menu').styles['border-top-color'],'rgb(230, 230, 230)')
check('menu shadow uses the recovered WorkBuddy popover token',measuredNode('.cr-model-selector__menu').styles['box-shadow'],'color(srgb 0 0 0 / 0.04) 0px 4px 12px -4px, color(srgb 0 0 0 / 0.04) 0px 3px 6px -8px')
check('list uses the source 2px row gap',measuredNode('.cr-model-selector__list').styles.gap,'2px')
check('all model rows retain source radius and inset',measurements.openState.nodes.filter(node=>node.selector==='.cr-model-selector__item').map(node=>[node.bounds[3],node.styles['border-radius'],node.styles['padding-left'],node.styles['padding-right']]),[[32,'8px','8px','8px'],[32,'8px','8px','8px'],[32,'8px','8px','8px'],[32,'8px','8px','8px']])
check('all model icons retain source 16px boxes',measurements.openState.nodes.filter(node=>node.selector==='.cr-model-selector__item-icon').map(node=>node.bounds.slice(2)),[[16,16],[16,16],[16,16],[16,16]])
check('all model icon SVGs retain source 16px boxes',measurements.openState.nodes.filter(node=>node.selector==='.cr-model-selector__item-icon svg').map(node=>node.bounds.slice(2)),[[16,16],[16,16],[16,16],[16,16]])
check('scroll wrapper and inner region are both measured',[
!!measuredNode('.cr-slim-scroll'),
!!measuredNode('.cr-model-selector__list'),
measuredNode('.cr-slim-scroll').styles.overflow,
measuredNode('.cr-model-selector__list').styles.overflow
],[true,true,'visible','hidden auto'])
check('all model label text nodes use source typography',measurements.openState.nodes.filter(node=>node.selector==='.cr-model-selector__item-name').map(node=>[node.styles['font-size'],node.styles['line-height'],node.styles.color,node.textBounds.length]),[['14px','22px','rgba(0, 0, 0, 0.9)',4],['14px','22px','rgba(0, 0, 0, 0.9)',4],['14px','22px','rgba(0, 0, 0, 0.9)',4],['14px','22px','rgba(0, 0, 0, 0.9)',4]])
check('selected label uses source semibold weight',measuredNode('.cr-model-selector__item-name',1).styles['font-weight'],'600')
check('model label uses 14px source size',measuredNode('.cr-model-selector__item-name').styles['font-size'],'14px')
check('model label uses 22px source line height',measuredNode('.cr-model-selector__item-name').styles['line-height'],'22px')
check('footer uses the source 32px height',measuredNode('.cr-menu-item').bounds[3],32)
check('footer text range is measured',measuredNode('.cr-menu-item__content').textBounds.length,4)
check('footer icon remains 16px with 8px spacing',[
measuredNode('.cr-menu-item__icon').bounds.slice(2),
measuredNode('.cr-menu-item__icon').styles['margin-right']
],[[16,16],'8px'])
check('footer source glyph occupies the full 16px icon box',measuredNode('.cr-menu-item__icon svg').bounds.slice(2),[16,16])
await capture('open')
// Reasoning sub-menu: the recovered body exposes it only when the option carries reasoning metadata.
await hoverRow()
await wait('!!document.querySelector(".cr-model-selector__sub-menu")')
measurements.hoverState=await visualSnapshot()
const hoveredRow=measurements.hoverState.nodes.find(node=>node.selector==='.cr-model-selector__item'&&node.classes.includes('--selected'))
check('selected reasoning row uses the recovered hover color while its submenu opens',hoveredRow.styles['background-color'],'rgb(242, 242, 242)')
await capture('hovered')
check('reasoning sub-menu opens for a reasoning model',await read('!!document.querySelector(".cr-model-selector__sub-menu")'),true)
check('sub-menu shows the current effort',await read('document.querySelector(".cr-model-selector__sub-menu-action")?.textContent??null'),'思考强度中')
// The effort panel is itself a nested SubMenu, revealed by hovering its action row.
await hoverSelector('.cr-model-selector__sub-menu-action')
await wait('!!document.querySelector(".cr-model-selector__sub-panel")')
check('thinking toggle is offered for a switchable model',await read('!!document.querySelector(".cr-model-selector__sub-panel-toggle input[role=switch]")'),true)
check('thinking toggle reflects the persisted level',await read('!!document.querySelector(".cr-model-selector__sub-panel-toggle input[role=switch]")?.checked'),true)
const effortRows=await read('[...document.querySelectorAll(".cr-model-selector__sub-panel-item")].map(e=>e.textContent)')
check('effort panel lists every supported level',effortRows.length,5)
check('current effort is marked active',await read('document.querySelector(".cr-model-selector__sub-panel-item--active")?.textContent??null'),'中')
await capture('reasoning-panel')
await clickText('.cr-model-selector__sub-panel-item','高')
await wait('window.qa.levels.length===1')
check('effort pick persists through the host callback',await read('window.qa.levels'),['high'])
await click('.cr-model-selector__sub-panel-toggle input[role=switch]')
await wait('window.qa.levels.length===2')
check('thinking toggle off persists the off level',await read('window.qa.levels'),['high','off'])
await read('window.qa.setLevel("medium")')
await pause(200)
// Re-enabling thinking restores the effort list; the toggle-off state hides it in the source body.
check('re-enabling thinking restores the effort list',await read('document.querySelectorAll(".cr-model-selector__sub-panel-item").length'),5)
const activeEffort=await read('document.querySelector(".cr-model-selector__sub-panel-item--active")?.textContent??null')
check('current effort is marked active',activeEffort,'中')
// Pick a different effort and confirm it is written through the host callback.
await clickText('.cr-model-selector__sub-panel-item','高')
await wait('window.qa.levels.length===3')
check('effort pick after re-enable persists through the host callback',await read('window.qa.levels'),['high','off','high'])
check('effort pick keeps the popover open as in source',await read('!!document.querySelector(".cr-model-selector__menu")'),true)
// The nested effort panel is placed over the trigger, exactly as the source lays it out, so the pointer has
// to leave the sub-menu before the trigger becomes clickable again.
await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',x:5,y:5})
await wait('!document.querySelector(".cr-model-selector__sub-panel")')
await click('.cr-model-selector__trigger')
await wait('!document.querySelector(".cr-model-selector__menu")')
await click('.cr-model-selector__trigger')
await wait('!!document.querySelector(".cr-model-selector__menu")')
await sendKey('Escape')
check('source popover closes on Escape',await read('!document.querySelector(".cr-model-selector__menu")'),true)
await click('.cr-model-selector__trigger')
await wait('!!document.querySelector(".cr-model-selector__menu")')
await click('.cr-model-selector__menu .cr-model-selector__item')
await wait('window.qa.picks.length===1')
check('pick writes through the real host callback',await read('window.qa.picks'),['ui-model-one'])
await wait('!document.querySelector(".cr-model-selector__menu")')
check('source body closes the popover after a pick',await read('!!document.querySelector(".cr-model-selector__menu")'),false)
await wait('document.querySelector(".cr-model-selector__trigger").textContent.includes("studio-mini-one")')
check('trigger follows the persisted selection',await read('document.querySelector(".cr-model-selector__trigger").textContent.includes("studio-mini-one")'),true)
await click('.cr-model-selector__trigger')
await wait('!!document.querySelector(".cr-model-selector__menu")')
await click('.cr-model-selector__menu > .cr-menu-item')
check('config footer opens the real settings entry',await read('window.qa.manages'),1)
await read('window.qa.setDisabled(true)');await pause(120)
check('disabled shows the source disabled class',await read('document.querySelector(".cr-model-selector__trigger").className.includes("cr-model-selector__trigger--disabled")'),true)
check('disabled hides the arrow as in source',await read('getComputedStyle(document.querySelector(".source-model-selector .wb-icon.cr-model-selector__trigger-arrow")).display'),'none')
check('disabled exposes aria-disabled',await read('document.querySelector(".cr-model-selector__trigger").getAttribute("aria-disabled")'),'true')
await read('window.qa.setDisabled(false);window.qa.setDark(true)');await pause(150)
// Regression: a model whose `defaultEffort` is an empty string must still resolve to a real level.
// Passing the empty string through made the host reject the write as an invalid effort.
await read('window.qa.levels.length=0;window.qa.setSelected("ui-model-four");window.qa.setLevel("off")');await pause(260)
await openSubMenu()
check('thinking-off model reports the off state',await read('document.querySelector(".cr-model-selector__sub-menu-action-value")?.textContent??null'),'关闭')
await click('.cr-model-selector__sub-panel-toggle input[role=switch]')
await wait('window.qa.levels.length===1')
check('re-enabling thinking persists the model effort',await read('window.qa.levels'),['max'])
check('empty defaultEffort resolves to a supported level',await read('document.querySelector(".cr-model-selector__sub-menu-action-value")?.textContent??null'),'极致')
await closePopover()
await read('window.qa.setSelected("ui-model-one");window.qa.setLevel("medium")');await pause(260)
check('dark scope marker applied',await read('document.querySelector(".yc-source-model-selector").dataset.crTheme'),'dark')
await capture('dark')
check('renderer raised no exception',errors,[])
fs.writeFileSync(path.join(output,'report.json'),JSON.stringify({result:'PASS',checks,measurements,scope:'Own Electron execution of the recovered fixed REA model-selector bodies; no original runtime access'},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,measurements,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)})