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_DISABLE_GPU: '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(), 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) }) }