// Capture stable module states in the production app. WorkBuddy references are // read only from retained REA Evidence; this harness controls YCode exclusively. const fs = require('node:fs/promises') const path = require('node:path') const assert = require('node:assert/strict') if (!process.versions.electron) { ;(async () => { await fs.mkdir(path.resolve('.tmp'), { recursive: true }) const directory = await fs.mkdtemp(path.resolve('.tmp/module-ui-parity-')) await Promise.all(['data', 'home', 'workspace'].map(name => fs.mkdir(path.join(directory, name)))) const data = path.join(directory, 'data'), cwd = path.join(directory, 'workspace'), now = Date.now() await fs.writeFile(path.join(cwd, 'report.md'), '# UI parity artifact\n') await fs.writeFile(path.join(data, 'models.json'), JSON.stringify({ version: 1, selectedModelId: 'parity-model', models: [ { id: 'parity-model', name: 'DeepSeek V4', providerId: 'deepseek', model: 'deepseek-v4', baseUrl: 'http://127.0.0.1:1/v1', authMode: 'none' } ] })) await fs.writeFile(path.join(data, 'workspaces.json'), JSON.stringify({ selectedWorkspaceId: 'parity-workspace', workspaces: [ { id: 'parity-workspace', name: 'YCode', root: cwd, createdAt: now } ] })) await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions: [{ id: 'parity-artifact', title: '生成项目报告', cwd, workspaceId: 'parity-workspace', modelId: 'parity-model', createdAt: now, updatedAt: now, messages: [{ id: 'artifact-user', role: 'user', content: '生成报告', createdAt: now - 1, status: 'done' }, { id: 'artifact-assistant', role: 'assistant', content: '报告已生成。', createdAt: now, status: 'done', parts: [], tools: [ { id: 'artifact-write', name: 'write_file', args: { path: 'report.md', content: '# UI parity artifact\n' }, argsRaw: '{}', status: 'ok' } ] }] }] })) const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_DISABLE_GPU: '1', YCODE_QA_OFFSCREEN: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' } delete env.ELECTRON_RUN_AS_NODE delete env.ELECTRON_RENDERER_URL 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', bytes => { log += bytes; process.stdout.write(bytes) }) child.stderr.on('data', bytes => { log += bytes }) const timer = setTimeout(() => child.kill(), 110_000) 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(-10000)) process.exitCode = code ?? 1 }) })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow } = require('electron') const { pathToFileURL } = require('node:url') const directory = process.env.YCODE_QA_DIRECTORY const relative = path.relative(path.resolve('.tmp'), directory || '') assert.ok(relative.startsWith('module-ui-parity-') && !relative.includes(path.sep)) app.setPath('home', path.join(directory, 'home')) let win, phase = 'boot' const screenshots = [], geometry = {}, errors = [], checks = [] const read = source => win.webContents.executeJavaScript(source) const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) const wait = async source => { for (let count = 0; count < 400; count++) { if (await read(source)) return; await pause(30) } throw Error(`Timeout ${phase}: ${source}`) } const rects = async selectors => read(`Object.fromEntries(${JSON.stringify(selectors)}.map(selector => { const el = document.querySelector(selector); if (!el) return [selector,null]; const r=el.getBoundingClientRect(),s=getComputedStyle(el); return [selector,{x:r.x,y:r.y,width:r.width,height:r.height,font:s.fontFamily,size:s.fontSize,weight:s.fontWeight, lineHeight:s.lineHeight,background:s.backgroundColor,color:s.color,radius:s.borderRadius,gap:s.gap,padding:s.padding, justify:s.justifyContent,borderTopWidth:s.borderTopWidth,disabledColor:s.getPropertyValue('--wb-button-plain-fg').trim()}] }))`) const capture = async (name, selectors = []) => { phase = name await pause(700) geometry[name] = await rects(selectors) const filename = `${name}.png` await fs.writeFile(path.join(directory, filename), (await win.webContents.capturePage()).toPNG()) screenshots.push(filename) } const navigate = async name => { await read(`Array.from(document.querySelectorAll('.conversation-list-tab-button')).find(el=>el.textContent.trim()===${JSON.stringify(name)}).click()`) } const setTheme = async theme => { await read(`localStorage.setItem('ycode:theme',${JSON.stringify(theme)});window.dispatchEvent(new StorageEvent('storage',{key:'ycode:theme',newValue:${JSON.stringify(theme)}}))`) await wait(`document.documentElement.dataset.theme===${JSON.stringify(theme)}`) } const baseline = process.env.YCODE_UI_PARITY_BASELINE === '1' const check = (label, condition) => { checks.push({ label, pass: Boolean(condition) }) if (!baseline) assert.ok(condition, label) } const closeEditor = async () => { await read(`document.querySelector('.automation-editor-modal button[aria-label="关闭"]').click()`) await wait(`!document.querySelector('.automation-editor-modal')`) } ;(async () => { await import(pathToFileURL(path.resolve('out/main/index.mjs')).href) await app.whenReady() for (let count = 0; count < 350; count++) { win = BrowserWindow.getAllWindows().find(item => item.getTitle() === 'YCode') if (win && !win.webContents.isLoading() && await read('!!document.querySelector(".sidebar") && !!window.ycode?.automation')) break await pause(30) } assert.ok(win) win.setSize(1336, 902) win.webContents.on('console-message', (...args) => { const event = args[0], level = typeof event === 'object' ? event.level : args[1] if (level === 'error' || Number(level) >= 3) errors.push(typeof event === 'object' ? event.message : args[2]) }) for (const theme of ['light', 'dark']) { await setTheme(theme) await navigate('定时任务') await wait(`document.querySelector('.automation-page') && document.body.innerText.includes('暂无定时任务')`) await capture(`automation-empty-${theme}`, ['.sidebar','.automation-page','.automation-toolbar','.automation-empty-state','.automation-template-card']) await read('document.querySelector(".automation-create-main").click()') await wait(`!!document.querySelector('.automation-editor-modal')`) await capture(`automation-editor-${theme}`, ['.automation-editor-modal','.automation-name-input','.automation-prompt-composer','.automation-prompt-input','.atm-schedule-v2','.automation-editor-footer']) const editor = geometry[`automation-editor-${theme}`] // The original layout archive caught opacity:0 / scale(.96). Undo that // entrance frame instead of teaching the app to stay at its shrunken size. check(`${theme}: source stable editor width 800`, Math.abs(editor['.automation-editor-modal'].width - 800) < 1) check(`${theme}: source stable editor height 490`, Math.abs(editor['.automation-editor-modal'].height - 490) < 2) check(`${theme}: source name input height 40`, Math.abs(editor['.automation-name-input'].height - 40) < 1) check(`${theme}: source name input width 752`, Math.abs(editor['.automation-name-input'].width - 752) < 1) check(`${theme}: source editor typography`, editor['.automation-editor-modal'].font === '"PingFang SC", -apple-system, BlinkMacSystemFont, sans-serif') await read(`document.querySelector('[aria-label="重复频率"]').click()`) await wait(`document.querySelectorAll('.atm-freq-menu-popover [role="option"]').length === 7`) await capture(`automation-frequency-${theme}`, ['.atm-freq-menu-popover']) check(`${theme}: frequency uses source popover`, !await read(`!!document.querySelector('select[aria-label="重复频率"]')`)) await read(`document.querySelector('.atm-freq-menu-popover [role="option"][aria-selected="true"]').click()`) await read(`document.querySelector('.automation-name-input').focus()`) win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' }) win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' }) await pause(100) check(`${theme}: Escape keeps original automation editor open`, await read(`!!document.querySelector('.automation-editor-modal')`)) if (await read(`!!document.querySelector('.automation-editor-modal')`)) { await read(`document.querySelector('.automation-editor-overlay').dispatchEvent(new MouseEvent('mousedown',{bubbles:true}))`) check(`${theme}: overlay keeps original automation editor open`, await read(`!!document.querySelector('.automation-editor-modal')`)) if (await read(`!!document.querySelector('.automation-editor-modal')`)) await closeEditor() } await navigate('资料库') await wait(`!!document.querySelector('.library-page')`) await capture(`library-recent-${theme}`, ['.library-page','.library-sidebar','.library-content','.knowledge-sidebar-pc']) await read(`Array.from(document.querySelectorAll('.library-nav-item')).find(el=>el.textContent.includes('本地产物')).click()`) await wait(`!!document.querySelector('.library-file-row')`) await capture(`library-local-${theme}`, ['.library-page','.library-sidebar','.library-content','.library-local-table-header','.library-local-scroll','.library-file-row','.library-file-button']) check(`${theme}: local artifacts use full main panel`, !await read(`!!document.querySelector('.library-sidebar')`)) check(`${theme}: source updater column restored`, await read(`Array.from(document.querySelectorAll('.library-page th, .library-local-table-header .my-files-flat-col-author')).some(el=>el.textContent.includes('更新人'))`)) check(`${theme}: local artifacts preserve actual source file`, await read(`document.querySelector('.library-file-button')?.textContent.includes('report.md')`)) const local = geometry[`library-local-${theme}`] check(`${theme}: local table spans main area`, Math.abs(local['.library-local-table-header'].width - 1004) < 1) check(`${theme}: source local table header position`, Math.abs(local['.library-local-table-header'].y - 150) < 1) check(`${theme}: source local table scroll reaches window bottom`, Math.abs(local['.library-local-scroll'].y + local['.library-local-scroll'].height - 902) < 1) await read('document.querySelector(".conversation-list-tab-button-more").click()') await wait('!!document.querySelector(".yc-project-more button")') await read('document.querySelector(".yc-project-more button").click()') await wait('!!document.querySelector(".landing-header")') await capture(`project-list-${theme}`, ['.landing-header','.landing-template-card','.workbuddy-topbar']) await read('document.querySelector(".landing-new-btn").click()') await wait('!!document.querySelector(".collab-modal__container")') await capture(`project-create-${theme}`, ['.collab-modal__container','#yc-project-name','#yc-project-instructions']) await read(`Array.from(document.querySelectorAll('.collab-modal__container button')).find(el=>el.textContent.trim()==='取消').click()`) await wait('!document.querySelector(".collab-modal__container")') } const project = await read(`window.ycode.projects.create({name:'项目UI验收',instructions:'项目约束内容',experts:[],skills:[],connectors:[]})`) await navigate('资料库') await read('document.querySelector(".conversation-list-tab-button-more").click()') await wait('!!document.querySelector(".yc-project-more button")') await read('document.querySelector(".yc-project-more button").click()') await wait('!!document.querySelector(".project-grid__card[role=button]")') await read('document.querySelector(".project-grid__card[role=button]").click()') await wait('!!document.querySelector(".project-detail-view")') for (const theme of ['dark','light']) { await setTheme(theme) await capture(`project-detail-${theme}`, [ '.project-detail-view > .workbuddy-topbar', '.project-detail-view__main', '.project-detail-view__panel', '.project-detail-view__project-composer', '.project-composer__controls .cr-input-container', '.project-composer__controls .cr-input-box__main', '.project-composer__controls .wb-composer__workspace-footer', '.project-composer__controls .wb-composer__workspace-slot', '.project-composer__controls .cr-permission-setting', '.project-composer__controls .cr-send-button', '.project-detail-view__members-btn', '.project-detail-view__breadcrumb-domain-icon' ]) const detail = geometry[`project-detail-${theme}`] check(`${theme}: source project topbar height 64`, Math.abs(detail['.project-detail-view > .workbuddy-topbar'].height - 64) < 1) for (const selector of ['.project-detail-view__main','.project-detail-view__panel']) { check(`${theme}: ${selector} fills available height`, Math.abs(detail[selector].y + detail[selector].height - 902) < 1) check(`${theme}: ${selector} source typography`, detail[selector].font === '"PingFang SC", -apple-system, BlinkMacSystemFont, sans-serif') } check(`${theme}: project composer stays at bottom`, Math.abs(detail['.project-detail-view__project-composer'].y + detail['.project-detail-view__project-composer'].height - 878) < 1) const workspace = detail['.project-composer__controls .wb-composer__workspace-slot'] const permission = detail['.project-composer__controls .cr-permission-setting'] const footer = detail['.project-composer__controls .wb-composer__workspace-footer'] const mainArea = detail['.project-composer__controls .cr-input-box__main'] const send = detail['.project-composer__controls .cr-send-button'] check(`${theme}: project permission follows workspace`, Math.abs(permission.x - workspace.x - workspace.width) <= 8) check(`${theme}: project footer is left aligned with source gap`, footer.justify === 'flex-start' && footer.gap === '4px' && footer.padding === '0px 12px') check(`${theme}: empty project send button disabled`, await read(`document.querySelector('.project-composer__controls .cr-send-button').disabled`)) check(`${theme}: project source folder icon`, await read(`document.querySelector('.project-detail-view__breadcrumb-domain-icon path')?.getAttribute('d')?.startsWith('M5.1578')`)) if (theme === 'dark') { check('dark: project input source background', detail['.project-composer__controls .cr-input-container'].background === 'rgb(31, 31, 31)') check('dark: project main area source background', mainArea.background === 'rgb(31, 31, 31)') check('dark: project input card source radius and border', detail['.project-composer__controls .cr-input-container'].radius === '16px' && detail['.project-composer__controls .cr-input-container'].borderTopWidth === '1px') check('dark: disabled invite keeps source primary appearance', detail['.project-detail-view__members-btn'].background === 'rgba(255, 255, 255, 0.9)' && detail['.project-detail-view__members-btn'].color === 'rgba(0, 0, 0, 0.9)') check('dark: disabled send uses source foreground mix', send.disabledColor.includes('color-mix') && send.disabledColor.includes('40%')) } check(`${theme}: project detail uses the WorkBuddy font stack`, await read(`[ '.project-detail-view > .workbuddy-topbar', '.project-detail-view__main', '.project-detail-view__panel', '.project-detail-view__project-composer' ].every(selector => { const family=getComputedStyle(document.querySelector(selector)).fontFamily return family.includes('PingFang SC') && !/(Segoe UI|Roboto|Helvetica Neue)/.test(family) })`)) } win.setSize(900, 700) await capture('project-detail-narrow', ['.project-detail-view__main','.project-detail-view__panel','.project-detail-view__project-composer']) const narrow = geometry['project-detail-narrow']['.project-detail-view__main'] check('narrow: project resizes with viewport height', Math.abs(narrow.y + narrow.height - 700) < 1) check('module renderer has no console errors', errors.length === 0) const sources = [ ['automationEditor','.tmp/rea-todo20-scheduled-create.json'], ['automationPage','.tmp/rea-todo20-scheduled-page.json'], ['libraryRecent','.tmp/rea-four-modules-library-stable-20261017.json'], ['libraryLocal','.tmp/rea-four-modules-library-local-20261017.json'], ['projectList','.tmp/rea-todo20-project-page.json'], ['projectDetail','.tmp/rea-todo20-project-detail.json'] ] const references = {} for (const [name, filename] of sources) { const e = JSON.parse((await fs.readFile(filename,'utf8')).replace(/^\uFEFF/,'')) references[name] = { evidenceId:e.evidence_id, archive:path.resolve(filename), viewport:e.normalized_result.visuals.viewport } } await fs.writeFile(path.join(directory,'report.json'),JSON.stringify({ result:baseline?'BASELINE':'PASS',checks,geometry,screenshots,references,project,rendererErrors:errors, limitations:['Stable-state geometry and screenshots; not a full-state zero-pixel-difference claim.','Original and candidate data differ; no cloud example data is copied.'] },null,2)) console.log(`MODULE_UI_PARITY_${baseline ? 'BASELINE' : 'PASS'} ${directory}`) app.exit(0) })().catch(async error => { console.error(error) if(win&&!win.isDestroyed()) await fs.writeFile(path.join(directory,'failure.png'),(await win.webContents.capturePage()).toPNG()).catch(()=>{}) await fs.writeFile(path.join(directory,'failure.json'),JSON.stringify({phase,error:String(error.stack||error),checks,geometry,rendererErrors:errors},null,2)) app.exit(1) }) }