// Isolated YCode renderer behavior checks. The bridge is a deterministic local fixture. const path = require('node:path') const fs = require('node:fs/promises') const assert = require('node:assert/strict') const directory = path.resolve('.tmp/workspace-ui-regression') if (!process.versions.electron) { ;(async () => { await fs.mkdir(directory, { recursive: true }) const entry = ` import React from 'react';import {createRoot} from 'react-dom/client'; import Sidebar from './src/renderer/src/components/Sidebar'; import {useSettingsStore as settings} from './src/renderer/src/stores/settings'; import {useChatStore as chat} from './src/renderer/src/stores/chat'; import './src/renderer/src/workbuddy/typography/tokens.css'; import './src/renderer/src/styles/tokens.css';import './src/renderer/src/styles/app.css';import './src/renderer/src/styles/sidebar.css'; const initial=[{id:'a',name:'项目A',root:'C:/fixture/A',createdAt:1},{id:'b',name:'项目B',root:'C:/fixture/B',createdAt:2}]; let backend={workspaces:initial,selectedWorkspaceId:'a'}; const calls={generate:[],attach:0,select:[],rename:[],remove:[],open:[],createSession:0,fail:false}; window.ycode={workspace:{ generate:async(name)=>{calls.generate.push(name);await new Promise(r=>setTimeout(r,25));if(calls.fail)throw Error('Fixture: directory creation failed');backend={workspaces:[...backend.workspaces,{id:'generated',name,root:'C:/fixture/'+name,createdAt:3}],selectedWorkspaceId:'generated'};return backend}, attach:async()=>{calls.attach++;return backend}, select:async(id)=>{calls.select.push(id);backend={...backend,selectedWorkspaceId:id};return backend}, rename:async(id,name)=>{calls.rename.push({id,name});backend={...backend,workspaces:backend.workspaces.map(s=>s.id===id?{...s,name}:s)};return backend}, remove:async(id)=>{calls.remove.push(id);backend={workspaces:backend.workspaces.filter(s=>s.id!==id),selectedWorkspaceId:null};return backend}, open:async(id)=>{calls.open.push(id)} },session:{create:async()=>{calls.createSession++;throw Error('Sidebar must not persist an empty session')},messages:async()=>[],remove:async()=>{}}}; const sessions=[...Array.from({length:7},(_,i)=>({id:'a'+i,title:'任务'+i,workspaceId:'a',cwd:'C:/fixture/A',createdAt:i,updatedAt:Date.now()})), {id:'outside',title:'独立任务',createdAt:0,updatedAt:Date.now()},{id:'removed',title:'已移除空间的任务',workspaceId:'missing',createdAt:0,updatedAt:Date.now()}]; settings.setState({...backend,selectedModelId:'fixture-model'});chat.setState({sessions}); window.qa={settings,chat,calls}; createRoot(document.getElementById('root')).render({}}/>); ` await require('esbuild').build({ stdin: { contents: entry, loader: 'tsx', resolveDir: process.cwd() }, outfile: path.join(directory, 'fixture.js'), bundle: true, platform: 'browser', format: 'iife', jsx: 'automatic', logLevel: 'silent' }) await fs.writeFile(path.join(directory, 'index.html'), '
') const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE const result = require('node:child_process').spawnSync(require('electron'), [__filename], { env, windowsHide: true, stdio: 'inherit', timeout: 60000 }) process.exitCode = result.status ?? 1 })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow, nativeImage } = require('electron') app.setPath('userData', path.join(directory, 'user-data')) app.disableHardwareAcceleration() let win const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) const read = code => win.webContents.executeJavaScript(code) async function until(code) { for (let index = 0; index < 150; index++) { if (await read(code)) return; await pause(20) } throw Error('Missing state: ' + code) } async function click(selector) { await read(`document.querySelector(${JSON.stringify(selector)}).click()`); await pause(30) } async function type(value, selector = '.create-workspace-modal__input input') { await read(`(()=>{const input=document.querySelector(${JSON.stringify(selector)});Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(input,${JSON.stringify(value)});input.dispatchEvent(new Event('input',{bubbles:true}))})()`) await pause(20) } const openCreate = () => click('.conversation-section-add-btn') app.whenReady().then(async () => { win = new BrowserWindow({ width: 1336, height: 901, useContentSize: true, show: false, webPreferences: { contextIsolation: false, nodeIntegration: false, offscreen: true } }) await win.loadFile(path.join(directory, 'index.html')) await until('!!document.querySelector(".conversation-section-add-btn")') assert.equal(await read('document.querySelector(".conversation-section-label-tasks").textContent.trim()'), '任务 (2)') assert.equal(await read('document.querySelectorAll("[data-workspace-id=a] [data-session-id]").length'), 5) assert.equal(await read('document.querySelectorAll("[data-workspace-id=b] [data-session-id]").length'), 0) await click('[data-workspace-id=a] .conversation-show-more-button') assert.equal(await read('document.querySelectorAll("[data-workspace-id=a] [data-session-id]").length'), 7) await click('.conversation-section-compact-btn') assert.equal(await read('[...document.querySelectorAll(".workspace-group-header")].every(e=>e.getAttribute("aria-expanded")==="false")'), true) await win.reload(); await until('!!document.querySelector(".conversation-section-add-btn")') assert.equal(await read('[...document.querySelectorAll(".workspace-group-header")].every(e=>e.getAttribute("aria-expanded")==="false")'), true, 'collapsed state persists across page reload') await click('.conversation-section-compact-btn') await openCreate() assert.equal(await read('document.activeElement.getAttribute("aria-label")'), '工作空间名称') assert.equal(await read('document.querySelector(".create-workspace-modal__confirm").disabled'), true) await pause(250) const emptyImage = await win.webContents.capturePage() await fs.writeFile(path.join(directory, 'create-dialog-empty.png'), emptyImage.toPNG()) let pixels = null const sourcePath = path.resolve('.tmp/rea-assessment-create-space/frame-000.png') const sourceBytes = await fs.readFile(sourcePath).catch(() => null) if (sourceBytes) { assert.equal(require('node:crypto').createHash('sha256').update(sourceBytes).digest('hex'), '1df565bcbe19fb98b522f0346c1159caef338a1ce65c921baf8600bf27fb27ee', 'fixed REA reference changed') const reference = nativeImage.createFromBuffer(sourceBytes) assert.deepEqual(emptyImage.getSize(), reference.getSize()) const source = reference.toBitmap(), candidate = emptyImage.toBitmap(), width = reference.getSize().width const region = { x: 453, y: 350, width: 430, height: 203 } const caretMask = { x: 462, y: 458, width: 1, height: 19 } let compared = 0, changed = 0, maximumDelta = 0 for (let y = region.y; y < region.y + region.height; y++) for (let x = region.x; x < region.x + region.width; x++) { if (x === caretMask.x && y >= caretMask.y && y < caretMask.y + caretMask.height) continue const offset = (y * width + x) * 4 let delta = 0 for (let channel = 0; channel < 4; channel++) delta = Math.max(delta, Math.abs(source[offset + channel] - candidate[offset + channel])) compared++; if (delta) changed++; maximumDelta = Math.max(maximumDelta, delta) } pixels = { sourceEvidenceId: 'ev_1e096ecf360249286260ceef24f1195919b8f1cbae8dd81751a2e36eb631bab2', sourcePath, region, caretMask, compared, changed, maximumDelta, scope: 'Create dialog interior only; excludes outer shadow/background, other states and the blinking caret.' } assert.equal(changed, 0, 'source create-dialog interior must match the pinned REA pixels exactly, except blinking caret') } await type(' ') assert.equal(await read('document.querySelector(".create-workspace-modal__confirm").disabled'), true) await type(' 测试空间 ') await read(`document.querySelector('.create-workspace-modal__input input').dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',keyCode:229,isComposing:true,bubbles:true}))`) assert.equal(await read('window.qa.calls.generate.length'), 0, 'IME Enter does not submit') const metrics = await read(`(()=>{const m=document.querySelector('.create-workspace-modal');return{bounds:m.getBoundingClientRect().toJSON(),radius:getComputedStyle(m).borderRadius,padding:getComputedStyle(m).padding,inputHeight:getComputedStyle(document.querySelector('.create-workspace-modal__input')).height}})()`) assert.equal(metrics.bounds.width, 480) assert.equal(metrics.radius, '24px') assert.equal(metrics.padding, '24px') assert.equal(metrics.inputHeight, '32px') await pause(250) await fs.writeFile(path.join(directory, 'create-dialog.png'), (await win.webContents.capturePage()).toPNG()) await read(`(()=>{const b=document.querySelector('.create-workspace-modal__confirm');b.click();b.click()})()`) await until('!document.querySelector(".create-workspace-modal")') assert.deepEqual(await read('window.qa.calls.generate'), ['测试空间'], 'rapid repeated clicks create one trimmed workspace') assert.equal(await read('window.qa.calls.createSession'), 0, 'creating a workspace does not create a conversation') assert.equal(await read('!!document.querySelector("[data-workspace-id=generated]")'), true) await click('[data-workspace-id=generated] .workspace-new-task-button') assert.deepEqual(await read('window.qa.chat.getState().draftContext'), { workspaceId: 'generated', cwd: 'C:/fixture/测试空间', modelId: 'fixture-model' }) assert.equal(await read('window.qa.calls.createSession'), 0, 'space new task remains a draft until first send') await openCreate(); await type('未保存'); await read('document.dispatchEvent(new KeyboardEvent("keydown",{key:"Escape",bubbles:true}))') await until('!document.querySelector(".create-workspace-modal")') assert.equal(await read('window.qa.calls.generate.length'), 1, 'Escape cancels without creating') await openCreate(); await read('window.qa.calls.fail=true'); await type('失败空间'); await click('.create-workspace-modal__confirm') await until('!!document.querySelector(".workspace-dialog-error")') assert.equal(await read('document.querySelector(".create-workspace-modal__input input").value'), '失败空间') assert.equal(await read('document.querySelector(".create-workspace-modal__confirm").disabled'), false, 'creation failure keeps an editable retry') await click('.create-workspace-modal__cancel') assert.equal(await read('!!document.querySelector(".workspace-attach-row:not(.conversation-show-more-button)")'), false, 'existing-folder picker belongs in the home picker, not an extra sidebar row') await read('document.querySelector("[data-workspace-id=a] .workspace-group-header").dispatchEvent(new MouseEvent("contextmenu",{clientX:1335,clientY:900,bubbles:true,cancelable:true}))') await until('!!document.querySelector(".workspace-more-popover")') assert.equal(await read('(()=>{const r=document.querySelector(".workspace-more-popover").getBoundingClientRect();return r.left>=4&&r.right<=innerWidth-4&&r.top>=4&&r.bottom<=innerHeight-4})()'), true, 'context menus stay inside screen edges') await read('document.dispatchEvent(new KeyboardEvent("keydown",{key:"Escape",bubbles:true}))') await until('!document.querySelector(".workspace-more-popover")') await click('[data-workspace-id=a] .workspace-more-button') await click('.workspace-more-popover__item') assert.deepEqual(await read('window.qa.calls.open'), ['a']) await click('[data-workspace-id=a] .workspace-more-button') await click('.workspace-more-popover li:nth-child(2) button') const longName = '很长的中文空间名称'.repeat(10) await type(longName); await click('.create-workspace-modal__confirm') await until('!document.querySelector(".create-workspace-modal")') assert.equal(await read('window.qa.settings.getState().workspaces.find(s=>s.id==="a").root'), 'C:/fixture/A', 'rename changes only display name') assert.equal(await read('(()=>{const e=document.querySelector("[data-workspace-id=a] .collapsible-section-label");return e.scrollWidth>e.clientWidth})()'), true, 'long workspace names ellipsize') await click('[data-workspace-id=a] .workspace-more-button'); await click('.workspace-more-popover li:nth-child(3) button') assert.match(await read('document.querySelector(".create-workspace-modal__desc").textContent'), /7 个任务/) await click('.create-workspace-modal__confirm'); await until('!document.querySelector(".create-workspace-modal")') assert.equal(await read('!!document.querySelector("[data-workspace-id=a]")'), false) assert.equal(await read('document.querySelector(".conversation-section-label-tasks").textContent.trim()'), '任务 (9)', 'removal retains all conversations as independent tasks') assert.deepEqual(await read('window.qa.calls.remove'), ['a']) const report = { result: 'PASS workspace UI', checks: ['source dimensions', 'real store bridge calls', 'group counts', '5-item show-more', 'collapse persistence', 'trim and duplicate-submit guard', 'IME', 'cancel', 'error retry', 'first-send draft boundary', 'open/rename/remove menus', 'long names', 'task retention'], createDialogMetrics: metrics, pixels } await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2)) console.log(JSON.stringify(report)) app.exit(0) }).catch(error => { console.error(error); app.exit(1) }) }