Files
ycode/scripts/verify-capability-center.cjs
T

98 lines
13 KiB
JavaScript

// Isolated production Electron UI. No WorkBuddy runtime access, fake IPC, installs or messages.
const fs = require('node:fs/promises'), path = require('node:path'), assert = require('node:assert/strict')
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/capability-center-'))
await Promise.all(['data', 'home'].map(name => fs.mkdir(path.join(directory, name))))
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: path.join(directory, 'data'), YCODE_OMP_HOME: path.join(directory, 'home'), YCODE_QA_OFFSCREEN: '0' }
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL; delete env.YCODE_DISABLE_GPU
const child = require('node:child_process').spawn(require('electron'), ['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
let log = ''; child.stdout.on('data', b => { log += b; if (b.toString().includes('CAPABILITY_QA')) process.stdout.write(b) }); child.stderr.on('data', b => { log += b })
const timer = setTimeout(() => child.kill(), 90000)
child.once('exit', async code => { clearTimeout(timer); await fs.writeFile(path.join(directory, 'process.log'), log); console.log(JSON.stringify({ directory, exitCode: code })); if (code) console.error(log.slice(-5000)); process.exitCode = code ?? 1 })
})().catch(error => { console.error(error); process.exitCode = 1 })
} else {
const { app, BrowserWindow } = require('electron'), { pathToFileURL } = require('node:url')
const directory = process.env.YCODE_QA_DIRECTORY
app.disableHardwareAcceleration(); app.setPath('userData', path.join(directory, 'data')); app.setPath('sessionData', path.join(directory, 'data')); app.setPath('home', path.join(directory, 'home'))
const checks = [], errors = [], layouts = [], requests = [], screenshots = []
let main
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
const read = code => main.webContents.executeJavaScript(code)
async function wait(code) { for (let i = 0; i < 240; i++) { if (await read(code)) return; await pause(25) } throw Error('Wait failed: '+code.slice(0,160)) }
function check(label, actual, expected = true) { const pass = JSON.stringify(actual) === JSON.stringify(expected); checks.push({ label, pass, actual, expected }); if (!pass) throw Error(label) }
async function clickText(selector, text) { await read(`(()=>{const n=[...document.querySelectorAll(${JSON.stringify(selector)})].find(n=>n.textContent.trim()===${JSON.stringify(text)});if(!n)throw Error('Missing UI button');n.click()})()`); await pause(80) }
async function type(value) { await read(`(()=>{const n=document.querySelector('.ec-topbar input');const setter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;setter.call(n,${JSON.stringify(value)});n.dispatchEvent(new Event('input',{bubbles:true}))})()`); await pause(380) }
async function metrics(width) { await main.webContents.debugger.sendCommand('Emulation.setDeviceMetricsOverride', { width, height: 901, deviceScaleFactor: 1, mobile: false }); await pause(160) }
async function snapshot(name) {
await read('document.fonts.ready.then(()=>true)'); await pause(200)
const layout = await read(`(()=>{const selectors=['.yc-capability-center','.ec-topbar','.um-header__tabs','.ec-topbar__right','.ec-my-created-empty','.ec-my-created-empty-icon','.ec-my-created-empty-title','[data-expert-page]','._grid_1tpa4_18','._card_1hdcm_2','[role=dialog]'];return {viewport:[innerWidth,innerHeight],theme:document.documentElement.dataset.theme,nodes:selectors.map(selector=>{const n=document.querySelector(selector);if(!n)return{selector,missing:true};const r=n.getBoundingClientRect(),s=getComputedStyle(n);return{selector,bounds:[r.x,r.y,r.width,r.height],fontSize:s.fontSize,fontFamily:s.fontFamily,color:s.color,background:s.backgroundColor,maskImage:s.maskImage}})}})()`)
layouts.push({ name, ...layout }); const image = await main.webContents.debugger.sendCommand('Page.captureScreenshot', { format:'png',fromSurface:true,captureBeyondViewport:false })
const file = path.join(directory, name+'.png'); await fs.writeFile(file, Buffer.from(image.data,'base64')); screenshots.push(file); return layout
}
;(async () => {
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
for (let i = 0; i < 240; i++) { main = BrowserWindow.getAllWindows().find(w=>w.getTitle()==='YCode'); if(main&&!main.webContents.isLoading())break; await pause(25) }
assert.ok(main); main.showInactive(); main.webContents.setBackgroundThrottling(false)
main.webContents.on('console-message', event => { if(event.level==='error'||event.level>=3)errors.push(event.message) })
await wait('!!window.ycode&&!!document.querySelector(".source-home")')
await read('localStorage.setItem("ycode:sidebar-width","284.25");localStorage.setItem("ycode:sidebar-collapsed","0")'); main.webContents.reload(); await wait('!!document.querySelector(".source-home")')
main.webContents.debugger.attach('1.3'); await main.webContents.debugger.sendCommand('Network.enable')
main.webContents.debugger.on('message', (_event, method, params) => { if(method==='Network.requestWillBeSent'&&/^https?:/.test(params.request.url))requests.push(params.request.url) })
await metrics(1336); await clickText('.sidebar__tab', '专家·技能·连接器'); await wait('!!document.querySelector("[data-capability-tab=experts]")')
check('source three-tab labels', await read('[...document.querySelectorAll(".um-header__tabs button")].map(n=>n.textContent.trim())'), ['专家','技能','连接器'])
check('source topbar is 64px', await read('document.querySelector(".ec-topbar").getBoundingClientRect().height'), 64)
check('source tab widths', await read('[...document.querySelectorAll(".um-header__tabs button")].map(n=>n.getBoundingClientRect().width)'), [60,60,78])
check('source 24px tab spacing', await read('getComputedStyle(document.querySelector(".um-header__tabs")).gap'), '24px')
check('default has no owned or activatable online experts', await read('!document.querySelector("._card_1hdcm_2")&&document.querySelector("[data-expert-page]").textContent.includes("在线专家目录尚未接入")'))
await snapshot('market-unavailable')
await clickText('.ec-topbar button', '我的专家'); await wait('!!document.querySelector("[data-expert-page=mine]")')
check('source my-experts search placeholder', await read('document.querySelector(".ec-topbar input").placeholder'), '搜索我创建的专家')
check('source original empty icon is 64x64', await read('(()=>{const r=document.querySelector(".ec-my-created-empty-icon").getBoundingClientRect();return[r.width,r.height]})()'), [64,64])
check('original empty SVG actually loads as mask', await read('getComputedStyle(document.querySelector(".ec-my-created-empty-icon")).maskImage.includes("empty-created-expert")'))
check('empty state is source icon and title only', await read('document.querySelector(".ec-my-created-empty").children.length'), 2)
check('source empty icon Y coordinate', await read('document.querySelector(".ec-my-created-empty-icon").getBoundingClientRect().y'), 472.5)
check('source empty title Y coordinate', await read('document.querySelector(".ec-my-created-empty-title").getBoundingClientRect().y'), 548.5)
check('source my-experts toolbar is 32px tall', await read('document.querySelector(".ec-my-created-toolbar").getBoundingClientRect().height'), 32)
check('source create button width is 108px', await read('document.querySelector(".ec-topbar .wb-button--primary").getBoundingClientRect().width'), 108)
check('source create button font is 14px', await read('getComputedStyle(document.querySelector(".ec-topbar .wb-button--primary")).fontSize'), '14px')
check('source my-experts toolbar actions width is 300px', await read('document.querySelector(".ec-topbar__right").getBoundingClientRect().width'), 300)
await snapshot('my-experts-empty')
await type('不存在'); check('real source debounced search updates empty state', await read('document.querySelector(".ec-my-created-empty-title").textContent'), '未找到专家')
await read('document.querySelector(".wb-input__clear").click()'); await pause(380); check('native clear keeps focus in search', await read('document.activeElement===document.querySelector(".ec-topbar input")'))
await read('document.querySelector("[aria-label=返回专家中心]").click()'); await pause(100)
await clickText('.capability-text-button', '查看缓存参考目录'); check('nine source-card references visibly read-only', await read('document.querySelectorAll("._card_1hdcm_2").length'), 9)
check('no source summon buttons on reference cards', await read('document.querySelectorAll("._summonBtn_1hdcm_408").length'), 0)
await read('Promise.all([...document.querySelectorAll("._card_1hdcm_2")].flatMap(n=>n.getAnimations()).map(a=>a.finished.catch(()=>undefined))).then(()=>true)')
check('source center card height is 156px', await read('document.querySelector("._card_1hdcm_2").getBoundingClientRect().height'), 156)
await snapshot('reference-cards')
await read(`(()=>{const n=document.querySelector('.ec-topbar input');n.dispatchEvent(new CompositionEvent('compositionstart',{bubbles:true,data:''}));Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(n,'开发');n.dispatchEvent(new Event('input',{bubbles:true}))})()`); await pause(380)
check('source search does not filter during IME composition', await read('document.querySelectorAll("._card_1hdcm_2").length'), 9)
await read(`document.querySelector('.ec-topbar input').dispatchEvent(new CompositionEvent('compositionend',{bubbles:true,data:'开发'}))`); await pause(80)
check('source search applies completed IME text immediately', await read('document.querySelectorAll("._card_1hdcm_2").length'), 2)
await type('开发'); check('source local search filters archived cards', await read('document.querySelectorAll("._card_1hdcm_2").length'), 2)
await read('document.querySelector("._card_1hdcm_2").click()'); await wait('!!document.querySelector("[role=dialog]")')
check('reference detail cannot activate unavailable package', await read('document.querySelector("[role=dialog] button[disabled]").textContent'), '仅供参考')
await snapshot('reference-detail'); main.webContents.sendInputEvent({type:'keyDown',keyCode:'Escape'}); await wait('!document.querySelector("[role=dialog]")')
await read('window.ycode.app.openSettings("appearance")')
let settings; for(let i=0;i<160;i++){settings=BrowserWindow.getAllWindows().find(w=>w!==main);if(settings&&!settings.webContents.isLoading())break;await pause(25)} assert.ok(settings)
await settings.webContents.executeJavaScript(`[...document.querySelectorAll('.segmented__item')].find(n=>n.textContent.trim()==='深色').click()`)
await wait('document.documentElement.dataset.theme==="dark"')
check('capability center follows real shared dark theme', await read('!!document.querySelector(".yc-capability-center .expert-center-dark")'))
await snapshot('reference-dark')
await settings.webContents.executeJavaScript(`[...document.querySelectorAll('.segmented__item')].find(n=>n.textContent.trim()==='浅色').click()`)
await wait('document.documentElement.dataset.theme==="light"'); await settings.webContents.executeJavaScript(`document.querySelector('.settings-window-close').click()`)
await read('window.dispatchEvent(new CustomEvent("ycode:open-capabilities",{detail:{tab:"skills"}}))'); await wait('!!document.querySelector("[data-capability-tab=skills]")'); check('initialTab update receives skills management navigation', true)
await clickText('.um-header__tabs button','连接器'); await wait('!!document.querySelector("[data-capability-tab=connectors]")'); check('connectors tab mounts actual page', true)
await clickText('.um-header__tabs button','专家'); await metrics(480); await snapshot('narrow-480')
check('480px viewport has no document horizontal overflow', await read('document.documentElement.scrollWidth<=innerWidth'))
check('480px header remains inside center width', await read('(()=>{const h=document.querySelector(".ec-topbar").getBoundingClientRect();return h.right<=innerWidth+.1&&h.left>=0})()'))
await metrics(1336); await clickText('.ec-topbar button','我的专家'); await clickText('.ec-topbar button','创建专家'); await wait('!!document.querySelector(".source-home")')
check('create expert opens actual dialogue draft', await read('document.querySelector(".wb-composer__input").textContent.includes("专家")||document.querySelector(".wb-composer__input").value?.includes("专家")'))
check('no message or conversation persisted by browsing/creation entry', await read('window.ycode.session.list().then(items=>items.length)'), 0)
check('no WorkBuddy or other HTTP service requested', requests.length, 0); check('no renderer errors', errors.length, 0)
await fs.writeFile(path.join(directory,'report.json'),JSON.stringify({checks,layouts,screenshots,rendererErrors:errors,httpRequests:requests,limitations:['Current empty/offline reference states only; no expert install, execution, cloud catalog or full UI pixel parity claimed.']},null,2))
console.log('CAPABILITY_QA '+checks.filter(c=>c.pass).length+'/'+checks.length+' PASS'); app.exit(0)
})().catch(async error => { await fs.writeFile(path.join(directory,'report.json'),JSON.stringify({error:error.stack,checks,layouts,screenshots,rendererErrors:errors,httpRequests:requests},null,2)); console.error('CAPABILITY_QA FAIL '+error.stack); app.exit(1) })
}