feat: complete WorkBuddy parity and Windows release workflow

This commit is contained in:
xiaohan.yuan committed 2026-10-18 12:45:21 +08:00
1 parent 143516b3a8
commit d5512df549
434 files changed
+54977 -1854

No files matched your search

+277
View File
@@ -0,0 +1,277 @@
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/project-page-'))
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: path.join(directory, 'data'), YCODE_OMP_HOME: path.join(directory, 'home'), YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1' }
await fs.mkdir(env.YCODE_OMP_HOME, { recursive: true })
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(), 90000)
child.once('exit', async code => { clearTimeout(timer); await fs.writeFile(path.join(directory, 'process.log'), log); 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
app.setPath('home', path.join(directory, 'home'))
;(async () => {
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
await app.whenReady()
let win
const wait = async source => {
for (let i = 0; i < 300; i++) {
win = BrowserWindow.getAllWindows().find(item => item.getTitle() === 'YCode')
if (win && !win.webContents.isLoading() && await win.webContents.executeJavaScript(source)) return
await new Promise(resolve => setTimeout(resolve, 30))
}
throw Error(`Timeout: ${source}`)
}
await wait('!!window.ycode?.projects')
win.setContentSize(1336, 902)
await wait('innerHeight === 902')
await new Promise(resolve => setTimeout(resolve, 250))
const errors = []
win.webContents.on('console-message', (_event, level, message) => { if (level >= 3) errors.push(message) })
const read = source => win.webContents.executeJavaScript(source)
const dialogSurfaces = {}
const dialogBackgrounds = {}
const pixelAt = (image, point, viewport) => {
const size = image.getSize(), bitmap = image.toBitmap()
const x = Math.floor(point[0] * size.width / viewport[0]), y = Math.floor(point[1] * size.height / viewport[1])
const offset = (y * size.width + x) * 4
return [bitmap[offset+2],bitmap[offset+1],bitmap[offset]]
}
const rememberDialogBackground = async theme => {
// Theme surfaces transition for 150–200ms. Compare stable pixels, rather
// than a halfway theme frame against the fully transitioned modal frame.
await read(`Promise.all(document.getAnimations().filter(animation =>
Number.isFinite(animation.effect?.getComputedTiming().endTime)).map(animation => animation.finished.catch(()=>{})))`)
const region = await read(`(() => { const r=document.querySelector('.conversation-section-label-tasks').getBoundingClientRect();
return {point:[r.left+r.width/2,r.top+r.height/2],viewport:[innerWidth,innerHeight]} })()`)
dialogBackgrounds[theme] = {...region,pixel:pixelAt(await win.webContents.capturePage(),region.point,region.viewport)}
}
const verifyDialogSurface = async theme => {
await read('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
const surface = await read(`(() => {
const overlay = document.querySelector('.collab-modal__overlay')
const dialog = overlay.querySelector('.collab-modal__container')
const backdrop = overlay.querySelector('.collab-modal__backdrop')
const box = element => { const r = element.getBoundingClientRect(); return [r.x,r.y,r.width,r.height] }
const colors = Object.fromEntries(['container','header','body','footer'].map(part => [part,getComputedStyle(overlay.querySelector('.collab-modal__'+part)).backgroundColor]))
const dr = dialog.getBoundingClientRect()
const points = [[40,110],[innerWidth-35,180],[dr.left+25,dr.top+20],[dr.right-40,dr.bottom-30]]
return { theme:document.documentElement.dataset.theme, viewport:[innerWidth,innerHeight], overlay:box(overlay), dialog:box(dialog), colors,
mask:getComputedStyle(overlay).backgroundColor,zIndex:getComputedStyle(overlay).zIndex,
hitTargets:points.map(([x,y]) => { const hit=document.elementFromPoint(x,y); return {point:[x,y],className:hit?.className,
blocked:hit===backdrop || dialog.contains(hit)} }),
inputBackground:getComputedStyle(document.querySelector('#yc-project-name')).backgroundColor }
})()`)
const image = await win.webContents.capturePage()
const background = dialogBackgrounds[theme]
const alpha = Number(surface.mask.match(/,\s*([\d.]+)\)$/)?.[1] ?? 1)
surface.backgroundSample = {point:background.point,before:background.pixel,
expected:background.pixel.map(channel=>Math.round(channel*(1-alpha))),
actual:pixelAt(image,background.point,background.viewport)}
dialogSurfaces[theme] = surface
await fs.writeFile(path.join(directory, `project-dialog-${theme}.png`), image.toPNG())
await fs.writeFile(path.join(directory, 'dialog-surfaces.json'), JSON.stringify(dialogSurfaces,null,2))
const opaque = value => /^rgb\(/.test(value) || /, 1\)$/.test(value)
assert.ok(Object.values(surface.colors).every(opaque), `${theme}: dialog regions must be opaque`)
assert.deepEqual(surface.overlay, [0,0,...surface.viewport], `${theme}: mask covers the whole viewport`)
assert.ok(surface.hitTargets.every(item=>item.blocked), `${theme}: background chrome must stay below the modal mask`)
assert.ok(Number(surface.zIndex)>=101, `${theme}: modal owns a layer above the page chrome`)
assert.ok(surface.backgroundSample.actual.every((channel,index)=>Math.abs(channel-surface.backgroundSample.expected[index])<=2),
`${theme}: sticky sidebar headings must be visually dimmed by the mask`)
assert.equal(surface.dialog[2], 640)
}
const connector = await read(`window.ycode.mcp.save({name:'项目连接器',transport:'stdio',command:'node',args:['--version'],enabled:false})`)
const enter = async () => {
await wait('!!document.querySelector(".conversation-list-tab-button-more")')
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-new-btn")')
}
await enter()
await wait('!!document.querySelector(".project-grid__empty")')
assert.ok((await read('document.querySelector(".project-grid__empty").textContent')).includes('还没有项目'))
assert.equal(await read('document.querySelectorAll(".landing-template-card").length'), 5)
await rememberDialogBackground('light')
await read('document.querySelector(".landing-new-btn").click()')
await wait('!!document.querySelector("#yc-project-name")')
await verifyDialogSurface('light')
assert.equal(await read('document.querySelector(".collab-modal__footer button:last-child").disabled'), true)
await read(`(() => { const input = document.querySelector('#yc-project-name'); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, '项目验收'); input.dispatchEvent(new Event('input', { bubbles: true })); })()`)
await wait('!document.querySelector(".collab-modal__footer button:last-child").disabled')
await read('document.querySelector("[data-capability-kind=connectors] .cp-row__action").click()')
await wait('!!document.querySelector("[data-capability-kind=connectors] input[type=checkbox]")')
await read('document.querySelector("[data-capability-kind=connectors] input[type=checkbox]").click()')
await wait('document.querySelector("[data-capability-kind=connectors] .cp-chip__name")?.textContent === "项目连接器"')
await read('document.querySelector("[data-capability-kind=connectors] .cp-chip__x").click()')
await wait('!document.querySelector("[data-capability-kind=connectors] .cp-chip")')
await read('document.querySelector("[data-capability-kind=connectors] input[type=checkbox]").click()')
await read('document.querySelector(".collab-modal__footer button:last-child").click()')
await wait('!document.querySelector(".collab-modal__container") && document.querySelector(".project-grid__card-name-text")?.textContent === "项目验收"')
const records = JSON.parse(await fs.readFile(path.join(directory, 'data', 'projects.json'), 'utf8'))
assert.equal(records.length, 1); assert.equal(records[0].name, '项目验收')
assert.deepEqual(records[0].connectors, [connector.id])
const task = await read(`window.ycode.session.create('项目任务验收',{projectId:${JSON.stringify('REPLACE_PROJECT_ID')}})`.replace(JSON.stringify('REPLACE_PROJECT_ID'), JSON.stringify(records[0].projectId)))
assert.equal(task.projectId, records[0].projectId)
const invalidProjectRejected = await read(`window.ycode.session.create('无效项目任务',{projectId:'missing-project'}).then(()=>false,()=>true)`)
assert.equal(invalidProjectRejected, true)
const loaded = new Promise(resolve => win.webContents.once('did-finish-load', resolve))
win.webContents.reload()
await loaded
await wait('!!window.ycode?.projects')
await enter()
const restoredTask = await read(`window.ycode.session.list().then(items=>items.find(item=>item.id===${JSON.stringify('REPLACE_TASK_ID')}))`.replace(JSON.stringify('REPLACE_TASK_ID'), JSON.stringify(task.id)))
assert.equal(restoredTask.projectId, records[0].projectId)
await wait('document.querySelector(".project-grid__card-name-text")?.textContent === "项目验收"')
await read('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
await read(`(() => { const input = document.querySelector('input[aria-label="搜索项目"]'); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, '不存在'); input.dispatchEvent(new Event('input', { bubbles: true })); })()`)
await wait('document.querySelector(".project-grid__empty")?.textContent.includes("没有匹配的项目")')
await read(`(() => { const input = document.querySelector('input[aria-label="搜索项目"]'); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, ''); input.dispatchEvent(new Event('input', { bubbles: true })); })()`)
await wait('document.querySelector(".project-grid__card-name-text")?.textContent === "项目验收"')
await read('document.querySelector(".project-grid__card").click()')
await wait('!!document.querySelector(".project-detail-view")')
await read(`Array.from(document.querySelectorAll('.project-detail-view__top-tab')).find(item => item.textContent.trim() === '任务').click()`)
await wait('document.querySelector(".task-item")?.dataset.taskStatus === "idle"')
await read('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
const detailGeometry = await read(`(() => {
const rect = selector => { const el = document.querySelector(selector); if (!el) return null; const r = el.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height} }
return {
viewport: {width: innerWidth, height: innerHeight},
shell: rect('.app-shell'),
surface: rect('.yc-project-surface'),
detail: rect('.project-detail-view'),
topbar: rect('.project-detail-view > .workbuddy-topbar'),
header: rect('.project-detail-view__header'),
body: rect('.project-detail-view__body'),
main: rect('.project-detail-view__main'),
panel: rect('.project-detail-view__panel'),
composer: rect('.project-detail-view__project-composer'),
taskList: rect('.project-detail-view__task-list'),
toolbar: rect('.project-detail-view__task-toolbar'),
configCards: document.querySelectorAll('.context-panel .wb-config-card').length,
configKinds: document.querySelectorAll('[data-config-kind]').length
}
})()`)
console.log(`DETAIL_GEOMETRY ${JSON.stringify(detailGeometry)}`)
assert.equal(detailGeometry.topbar.height, 64)
assert.equal(detailGeometry.header, null)
assert.equal(detailGeometry.viewport.height, 902)
assert.equal(detailGeometry.body.height, 808)
assert.equal(detailGeometry.body.y, detailGeometry.topbar.y + detailGeometry.topbar.height)
assert.equal(detailGeometry.body.y + detailGeometry.body.height, detailGeometry.viewport.height)
assert.equal(detailGeometry.panel.width, 320)
assert.equal(detailGeometry.main.width + detailGeometry.panel.width, detailGeometry.body.width)
assert.equal(detailGeometry.composer.height, 148)
assert.equal(detailGeometry.composer.y, 730)
assert.equal(detailGeometry.composer.y + detailGeometry.composer.height, detailGeometry.viewport.height - 24)
assert.equal(detailGeometry.configCards, 5)
assert.equal(detailGeometry.configKinds, 3)
await read('document.querySelectorAll(".project-task-panel__filter-trigger")[0].click()')
await wait('document.querySelectorAll(".wb-select__option").length === 3')
assert.deepEqual(
await read('Array.from(document.querySelectorAll(".wb-select__option")).map(el => el.textContent.trim())'),
['全部任务', '个人任务', '协作任务'],
)
await read('document.querySelectorAll(".wb-select__option")[1].click()')
await wait('!document.querySelector(".wb-select__popover")')
assert.equal(await read('document.querySelectorAll(".task-item").length'), 1)
await read('document.querySelectorAll(".project-task-panel__filter-trigger")[0].click()')
await wait('document.querySelectorAll(".wb-select__option").length === 3')
await read('document.querySelectorAll(".wb-select__option")[2].click()')
await wait('!document.querySelector(".wb-select__popover") && !document.querySelector(".task-item")')
await read('document.querySelectorAll(".project-task-panel__filter-trigger")[0].click()')
await wait('document.querySelectorAll(".wb-select__option").length === 3')
await read('document.querySelectorAll(".wb-select__option")[0].click()')
await wait('!document.querySelector(".wb-select__popover") && document.querySelectorAll(".task-item").length === 1')
await read('document.querySelectorAll(".project-task-panel__filter-trigger")[1].click()')
await wait('document.querySelectorAll(".wb-select__option").length === 4')
assert.deepEqual(
await read('Array.from(document.querySelectorAll(".wb-select__option")).map(el => el.textContent.trim())'),
['全部来源', '手动创建', '定时任务', '本地'],
)
await read('document.querySelectorAll(".wb-select__option")[3].click()')
await wait('!document.querySelector(".wb-select__popover") && document.querySelectorAll(".task-item").length === 1')
await read('document.querySelectorAll(".project-task-panel__filter-trigger")[1].click()')
await wait('document.querySelectorAll(".wb-select__option").length === 4')
await read('document.querySelectorAll(".wb-select__option")[1].click()')
await wait('!document.querySelector(".wb-select__popover") && !document.querySelector(".task-item")')
await read('document.querySelectorAll(".project-task-panel__filter-trigger")[1].click()')
await wait('document.querySelectorAll(".wb-select__option").length === 4')
await read('document.querySelectorAll(".wb-select__option")[2].click()')
await wait('!document.querySelector(".wb-select__popover") && !document.querySelector(".task-item")')
await read('document.querySelectorAll(".project-task-panel__filter-trigger")[1].click()')
await wait('document.querySelectorAll(".wb-select__option").length === 4')
await read('document.querySelectorAll(".wb-select__option")[0].click()')
await wait('!document.querySelector(".wb-select__popover") && document.querySelectorAll(".task-item").length === 1')
await fs.writeFile(path.join(directory, 'project-detail-tasks.png'), (await win.webContents.capturePage()).toPNG())
const initialTaskStatus = await read('document.querySelector(".task-item").dataset.taskStatus')
assert.equal(initialTaskStatus, 'idle')
assert.equal(await read('document.querySelectorAll("[data-config-kind]").length'), 3)
await read('document.querySelector("[data-config-kind=connectors]").click()')
await wait('!!document.querySelector(".project-capability-editor")')
await wait('document.querySelector(".project-capability-editor")?.textContent.includes("项目连接器")')
assert.equal(await read('document.querySelector(".project-capability-editor__choice input[type=checkbox]").checked'), true)
await read('document.querySelector(".project-capability-editor__choice input[type=checkbox]").click()')
await wait('!document.querySelector(".project-capability-editor__choice input[type=checkbox]").checked')
await read('document.querySelector(".project-capability-editor .project-instruction-drawer__save").click()')
await wait('!document.querySelector(".project-capability-editor")')
const editedProject = await read(`window.ycode.projects.list().then(items => items.find(item => item.projectId === ${JSON.stringify(records[0].projectId)}))`)
assert.deepEqual(editedProject.connectors, [])
await read('document.querySelector(".project-detail-view__panel .wb-config-card .wb-config-card__hit-area").click()')
await wait('!!document.querySelector(".project-instruction-drawer__textarea")')
await read(`(() => { const input = document.querySelector(".project-instruction-drawer__textarea"); Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(input, '父对象同步测试'); input.dispatchEvent(new Event('input', { bubbles: true })); })()`)
await wait('!document.querySelector(".project-instruction-drawer__save").disabled')
await read('document.querySelector(".project-instruction-drawer__save").click()')
await wait('!document.querySelector(".project-instruction-drawer") && document.querySelector(".project-detail-view__panel").textContent.includes("父对象同步测试")')
await read('document.querySelector(".project-detail-view__breadcrumb button").click()')
await wait('document.querySelector(".project-grid__card-name-text")?.textContent === "项目验收"')
const geometry = await read(`(() => { const rect = selector => { const r = document.querySelector(selector).getBoundingClientRect(); return { x:r.x, y:r.y, width:r.width, height:r.height } }; return { topbar:rect('.workbuddy-topbar'), header:rect('.landing-header'), card:rect('.project-grid__card'), icon:rect('.project-grid__card-icon'), time:document.querySelector('.project-grid__card-desc').textContent } })()`)
assert.equal(geometry.topbar.height, 64)
assert.equal(geometry.header.y, geometry.topbar.y + 64 + 56)
assert.equal(geometry.card.height, 80)
assert.equal(geometry.icon.width, 36)
assert.ok(geometry.time.startsWith('添加于 '))
await fs.writeFile(path.join(directory, 'project-page.png'), (await win.webContents.capturePage()).toPNG())
await read(`document.documentElement.dataset.theme = 'dark'`)
await read('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
const dark = await read(`(() => { const card = getComputedStyle(document.querySelector('.project-grid__card')); const hero = getComputedStyle(document.querySelector('.landing-hero')); return { card:card.backgroundColor, text:card.color, heroFilter:hero.filter, heroOpacity:hero.opacity } })()`)
assert.notEqual(dark.card, 'rgb(255, 255, 255)')
assert.ok(dark.heroFilter.includes('invert(1)'))
assert.equal(dark.heroOpacity, '0.88')
await fs.writeFile(path.join(directory, 'project-page-dark.png'), (await win.webContents.capturePage()).toPNG())
await rememberDialogBackground('dark')
await read('document.querySelector(".landing-new-btn").click()')
await wait('!!document.querySelector(".collab-modal__container")')
await verifyDialogSurface('dark')
const dialog = await read(`(() => { const el = document.querySelector('.collab-modal__container'); const r=el.getBoundingClientRect(); return {width:r.width, background:getComputedStyle(el).backgroundColor, input:document.querySelector('#yc-project-name').maxLength} })()`)
assert.equal(dialog.width, 640)
assert.notEqual(dialog.background, 'rgb(255, 255, 255)')
assert.equal(dialog.input, 15)
await wait('!document.querySelector("button[aria-label=选择项目模板]").disabled')
await read(`(() => { const input = document.querySelector('#yc-project-name'); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, '保留项目名'); input.dispatchEvent(new Event('input', {bubbles:true})); })()`)
await read('document.querySelector("button[aria-label=选择项目模板]").click()')
await wait('document.querySelectorAll(".wb-select__option").length === 6')
await read(`document.querySelector('.wb-select__option').dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',bubbles:true}))`)
await wait('document.querySelector("#yc-project-instructions").value.includes("角色") && document.querySelectorAll("[data-capability-kind=experts] .cp-chip").length > 0')
assert.equal(await read('document.querySelector("#yc-project-name").value'), '保留项目名')
await read('document.querySelector("button[aria-label=选择项目模板]").click()')
await wait('document.querySelectorAll(".wb-select__option").length === 6')
await read(`Array.from(document.querySelectorAll('.wb-select__option')).find(el => el.textContent.trim() === '自定义').dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true}))`)
await wait('document.querySelector("#yc-project-instructions").value === "" && document.querySelectorAll(".cp-chip").length === 0')
assert.equal(await read('document.querySelector("#yc-project-name").value'), '保留项目名')
await fs.writeFile(path.join(directory, 'project-dialog-dark.png'), (await win.webContents.capturePage()).toPNG())
assert.deepEqual(errors, [])
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'PASS', records, initialTaskStatus, detailGeometry, editedProject, geometry, dark, dialog, dialogSurfaces, rendererErrors: errors }, null, 2))
console.log(`PROJECT_PAGE_PASS ${directory}`)
app.exit(0)
})().catch(error => { console.error(error); app.exit(1) })
}