54 lines
15 KiB
JavaScript
54 lines
15 KiB
JavaScript
/** Native typed-host fixture only. Does not inspect or interact with WorkBuddy. */
|
|
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,'expert-picker-native-'));assert.ok(path.relative(base,output).startsWith('expert-picker-native-')&&!path.relative(base,output).includes('..'));const 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-expert-picker-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('expert-picker-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 Picker from ${location('src/renderer/src/capabilities/SourceExpertPicker')};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')};
|
|
const original=[{id:'selected',name:'Selected',profession:'Current expert',payload:'HOST_SELECTED'},{id:'alpha',name:'Alpha',profession:'Alpha engineer',description:'DESCRIPTION_ONLY',payload:'HOST_ALPHA'},{id:'disabled',name:'Disabled',profession:'Unavailable expert',disabled:true},{id:'chinese',name:'中文设计',profession:'中文设计专家'},{id:'fifth',name:'Fifth expert'},...Array.from({length:7},(_,i)=>({id:'more-'+i,name:'More expert '+i}))].map(e=>({...e,searchText:('expert '+(e.profession||e.name)).toLowerCase()}));
|
|
window.__expertQA={calls:[],closes:0,manages:0,resolve:null};function Fixture(){const[open,setOpen]=useState(false),[scope,setScope]=useState('scope-A'),[busy,setBusy]=useState(false),[error,setError]=useState(''),[items,setItems]=useState(original),[mode,setMode]=useState('success');Object.assign(window.__expertQA,{setScope,setBusy,setError,setItems,setMode,open:()=>setOpen(true),original});return<div className="chat-pane cr-theme" style={{height:'100vh'}}><button id="trigger" style={{position:'absolute',left:400,top:600,width:100,height:32}} onClick={()=>setOpen(true)}>Experts</button>{open&&<Picker items={items} selectedId="selected" scopeKey={scope} busy={busy} error={error} anchor={document.getElementById('trigger')} onClose={()=>{window.__expertQA.closes++;setOpen(false)}} onManage={()=>{window.__expertQA.manages++}} onSelect={async item=>{window.__expertQA.calls.push(item);if(mode==='defer')await new Promise(resolve=>{window.__expertQA.resolve=resolve});if(mode==='failure')throw Error('CONTROLLED_ACTIVATION_FAILURE')}}/>}</div>};createRoot(document.getElementById('root')).render(<Fixture/>);`)
|
|
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:1052,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))
|
|
const 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(40)}throw Error('Timeout '+code)}
|
|
const settle=async()=>{await cdp('Page.captureScreenshot',{format:'png',fromSurface:true});await pause(80)}
|
|
const click=async selector=>{await read(`document.querySelector(${JSON.stringify(selector)})?.scrollIntoView({block:'nearest'})`);const point=await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)throw Error('Missing '+${JSON.stringify(selector)});const r=e.getBoundingClientRect();return{x:r.x+r.width/2,y:r.y+r.height/2}})()`);await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',...point});await pause(70);for(const type of ['mousePressed','mouseReleased'])await cdp('Input.dispatchMouseEvent',{type,...point,button:'left',clickCount:1});await settle()}
|
|
const key=async key=>{for(const type of ['keyDown','keyUp'])await cdp('Input.dispatchKeyEvent',{type,key,code:key,windowsVirtualKeyCode:({Escape:27,Enter:13,ArrowDown:40,ArrowUp:38,Home:36,End:35,Tab:9,' ':32})[key]??65});await settle()}
|
|
const query=async text=>{await click('.yc-expert-picker input');await read('document.querySelector(".yc-expert-picker input").select()');if(text)await cdp('Input.insertText',{text});else await key('Backspace');await settle()}
|
|
const open=async()=>{await click('#trigger');await wait('!!document.querySelector(".yc-expert-picker input")');await settle()}
|
|
const capture=async name=>fs.writeFileSync(path.join(output,name+'.png'),Buffer.from((await cdp('Page.captureScreenshot',{format:'png',fromSurface:true})).data,'base64'))
|
|
const bounds=selector=>read(`(()=>{const r=document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect();return[r.x,r.y,r.width,r.height]})()`)
|
|
try{
|
|
await wait('!!document.querySelector("#trigger")');await open()
|
|
check('normal source view includes five actual registry items',await read('document.querySelectorAll("[data-expert-id]").length'),5)
|
|
measurements.normal=await bounds('.yc-expert-picker [role=menu]');check('source panel width',measurements.normal[2],240);check('source right-start placement with offset4',measurements.normal.slice(0,2),[504,600]);check('source normal panel geometry',measurements.normal[3],273)
|
|
check('source row height',(await bounds('[data-expert-id="alpha"]'))[3],32);check('source avatar geometry',(await bounds('._avatar_14qob_99')).slice(2),[16,16]);check('source search geometry',(await bounds('._search_1mrhu_59')).slice(2),[224,32]);check('initial focus is search',await read('document.activeElement.matches(".yc-expert-picker input")'),true)
|
|
check('selected row uses source checked state',await read('document.querySelector("[data-expert-id=selected]").getAttribute("aria-checked")'),'true');check('disabled host row uses source disabled state',await read('document.querySelector("[data-expert-id=disabled]").getAttribute("aria-disabled")'),'true');await click('[data-expert-id="disabled"]');check('disabled row never activates',await read('window.__expertQA.calls.length'),0);await capture('normal')
|
|
await click('[data-expert-id="selected"]');await wait('!document.querySelector(".yc-expert-picker")');check('selected normal row reactivates once before successful close',await read('window.__expertQA.calls.length'),1);check('selected reactivation passes actual host item',await read('window.__expertQA.calls.at(-1).payload'),'HOST_SELECTED');check('close restores anchor focus',await read('document.activeElement.id'),'trigger')
|
|
await read('window.__expertQA.setMode("failure")');await open();await click('[data-expert-id="selected"]');await wait('document.querySelector(".yc-expert-picker [role=alert]")?.textContent.includes("CONTROLLED_ACTIVATION_FAILURE")');check('selected reactivation failure invokes host exactly once',await read('window.__expertQA.calls.length'),2);check('selected reactivation failure keeps menu and previous binding checked',await read('[!!document.querySelector(".yc-expert-picker"),document.querySelector("[data-expert-id=selected]").getAttribute("aria-checked")]'),[true,'true']);await key('Escape');await read('window.__expertQA.setMode("success")')
|
|
await open();await query('ALPHA');check('source case-insensitive search matches display cache',await read('[...document.querySelectorAll("[data-expert-id]")].map(e=>e.dataset.expertId)'),['alpha']);await capture('search');await key('Escape');check('first Escape clears source search and keeps menu open',await read('document.querySelector(".yc-expert-picker input")?.value'),'');await key('Escape');await wait('!document.querySelector(".yc-expert-picker")')
|
|
await open();await query('DESCRIPTION_ONLY');check('source search does not invent description matching',await read('document.querySelector("._empty_14qob_258").textContent'),'未找到专家');await click('[aria-label="清空搜索"]');await read('document.querySelector(".yc-expert-picker input").dispatchEvent(new CompositionEvent("compositionstart",{bubbles:true}))');await cdp('Input.insertText',{text:'中文'});await settle();check('IME composing preserves committed list',await read('document.querySelectorAll("[data-expert-id]").length'),5);await read('document.querySelector(".yc-expert-picker input").dispatchEvent(new CompositionEvent("compositionend",{bubbles:true,data:"中文"}))');await settle();check('IME end commits source query',await read('[...document.querySelectorAll("[data-expert-id]")].map(e=>e.dataset.expertId)'),['chinese']);await click('[aria-label="清空搜索"]')
|
|
await click('.yc-expert-picker input');await key('ArrowDown');await key('ArrowDown');await key('ArrowDown');check('keyboard traversal skips disabled row',await read('document.activeElement.dataset.expertId'),'chinese');await key('Escape');await open()
|
|
await click('[data-expert-id="alpha"] button');check('pin does not activate or close',await read('[window.__expertQA.calls.length,!!document.querySelector(".yc-expert-picker")]'),[2,true]);check('pin is first in normal source order',await read('document.querySelector("[data-expert-id]").dataset.expertId'),'alpha');check('pin persists by host scope',await read('JSON.parse(localStorage.getItem("ycode.experts.menu.v1:scope-A")).pinned'),['alpha'])
|
|
await read('window.__expertQA.setMode("defer")');await settle();await click('[data-expert-id="alpha"]');check('async activation keeps menu until success',await read('!!document.querySelector(".yc-expert-picker")'),true);check('host receives item extra fields intact',await read('window.__expertQA.calls.at(-1).payload'),'HOST_ALPHA');await click('[data-expert-id="alpha"]');check('pending synchronous guard blocks duplicate activation',await read('window.__expertQA.calls.length'),3);await read('window.__expertQA.resolve()');await wait('!document.querySelector(".yc-expert-picker")');check('successful activation persists recent identity',await read('JSON.parse(localStorage.getItem("ycode.experts.menu.v1:scope-A")).recent[0]'),'alpha')
|
|
await read('window.__expertQA.setMode("failure")');await open();await click('[data-expert-id="alpha"]');await wait('document.querySelector(".yc-expert-picker [role=alert]")?.textContent.includes("CONTROLLED_ACTIVATION_FAILURE")');check('activation failure keeps real retry menu',await read('!!document.querySelector(".yc-expert-picker")'),true);await capture('failure-state');await key('Escape')
|
|
await read('window.__expertQA.setMode("defer")');await open();await click('[data-expert-id="alpha"]');const beforeScopeClose=await read('window.__expertQA.closes');await read('window.__expertQA.setScope("scope-B")');await settle();await read('window.__expertQA.resolve()');await settle();check('stale async completion cannot close new scope',await read('window.__expertQA.closes'),beforeScopeClose);check('new scope has no inherited pins',await read('document.querySelector("[data-expert-id]").dataset.expertId'),'selected');check('new scope gets no stale recent write',await read('localStorage.getItem("ycode.experts.menu.v1:scope-B")'),null)
|
|
await read('window.__expertQA.setBusy(true)');await settle();await click('[data-expert-id="alpha"]');check('external busy suppresses activation',await read('window.__expertQA.calls.length'),5);await read('window.__expertQA.setBusy(false)');await settle();await click('[data-expert-manage]');check('summon more delegates real host management',await read('window.__expertQA.manages'),1);await wait('!document.querySelector(".yc-expert-picker")')
|
|
await read('window.__expertQA.setItems([])');await open();check('empty state is exact source locale',await read('document.querySelector("._empty_14qob_258").textContent'),'暂无可用专家');await read('window.__expertQA.setError("CONTROLLED_LOAD_ERROR")');await settle();check('load error uses source failure state',await read('document.querySelector("._empty_14qob_258").textContent'),'加载失败,请稍后重试');await key('Escape')
|
|
await read('window.__expertQA.setError("");window.__expertQA.setItems(window.__expertQA.original);window.__expertQA.setMode("success")');await open();await query('expert');check('search covers actual full pool beyond normal recent five',await read('document.querySelectorAll("[data-expert-id]").length'),12);check('source search scroll clip max338',(await bounds('._searchList_14qob_70'))[3],338);await capture('scroll-search')
|
|
await key('Escape');await key('Escape');await read('document.getElementById("trigger").style.left="900px"');await open();measurements.edge=await bounds('.yc-expert-picker [role=menu]');check('source placement flips to left on viewport edge',measurements.edge[0],656);check('source width survives edge flip',measurements.edge[2],240)
|
|
check('no renderer exceptions',errors,[]);fs.writeFileSync(path.join(output,'report.json'),JSON.stringify({result:'PASS',checks,measurements,scope:'Actual Electron component + controlled typed host; no original runtime pixel 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)})
|