1203 lines
77 KiB
JavaScript
1203 lines
77 KiB
JavaScript
import { app, BrowserWindow } from 'electron'
|
|
import assert from 'node:assert/strict'
|
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import process from 'node:process'
|
|
import console from 'node:console'
|
|
import { setTimeout } from 'node:timers/promises'
|
|
|
|
const directory = process.env.GOODBUDDY_SUPERVISOR_DIRECTORY
|
|
const artifacts = process.env.GOODBUDDY_SUPERVISOR_ARTIFACTS || directory
|
|
app.setPath('userData', join(directory, 'profile'))
|
|
app.commandLine.appendSwitch('force-device-scale-factor', '1')
|
|
app
|
|
.whenReady()
|
|
.then(async () => {
|
|
await mkdir(artifacts, { recursive: true })
|
|
const win = new BrowserWindow({
|
|
show: false,
|
|
width: 1440,
|
|
height: 1100,
|
|
useContentSize: true,
|
|
webPreferences: {
|
|
sandbox: true,
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
backgroundThrottling: false
|
|
}
|
|
})
|
|
const errors = []
|
|
win.webContents.on('console-message', (event) => {
|
|
if (event.level === 'error') errors.push(event.message)
|
|
})
|
|
const js = async (code) => {
|
|
try {
|
|
return await win.webContents.executeJavaScript(code)
|
|
} catch (error) {
|
|
throw new Error(`${code}\n${errors.join('\n')}`, { cause: error })
|
|
}
|
|
}
|
|
const settle = async () => {
|
|
await js(
|
|
'new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))'
|
|
)
|
|
await setTimeout(250)
|
|
}
|
|
const wait = async (code) => {
|
|
for (let i = 0; i < 600; i++) {
|
|
if (await js(code)) return
|
|
await setTimeout(25)
|
|
}
|
|
throw new Error(`Timeout: ${code}\n${errors.join('\n')}`)
|
|
}
|
|
const open = async (dense) => {
|
|
await win.loadURL(
|
|
process.env.GOODBUDDY_SUPERVISOR_URL + (dense ? '?dense=1' : '')
|
|
)
|
|
await wait('!!document.querySelector("#supervisor-tab-graph")')
|
|
await js('document.querySelector("#supervisor-tab-graph").click()')
|
|
await wait('!!document.querySelector(".supervisor-workspace__map")')
|
|
await js('document.fonts.ready')
|
|
await settle()
|
|
}
|
|
const reports = []
|
|
const checkDiscussion = async () => {
|
|
const report = await js(`(() => {
|
|
const editor = document.querySelector('.supervision-discussion-editor');
|
|
const box = e => { const r = e.getBoundingClientRect(); return {left:r.left, right:r.right, top:r.top, bottom:r.bottom}; };
|
|
const target = editor.querySelector('.supervision-discussion-editor__target');
|
|
const field = editor.querySelector('.field'), label = field.querySelector('span'), input = field.querySelector('textarea');
|
|
const footer = editor.querySelector('footer');
|
|
return { target:box(target), field:box(field), label:box(label), input:box(input), footer:box(footer),
|
|
buttons:[...footer.querySelectorAll('button')].map(box), align:getComputedStyle(footer).justifyContent,
|
|
clientWidth:editor.clientWidth, scrollWidth:editor.scrollWidth, value:input.value,
|
|
border:getComputedStyle(target).borderBottomWidth, disclosures:editor.querySelectorAll('details').length };
|
|
})()`)
|
|
assert(report.value.includes('本周已核对交付清单') && report.value.includes('原始依据:模拟会议记录'), 'Actual reference content is editable')
|
|
assert.equal(report.field.top - report.target.bottom, 16, 'Target to message spacing')
|
|
assert.equal(report.input.top - report.label.bottom, 8, 'Label to input spacing')
|
|
assert.equal(report.footer.top - report.field.bottom, 16, 'Message to footer spacing')
|
|
assert.equal(report.border, '1px', 'Destination is separated')
|
|
assert.equal(report.align, 'flex-end', 'Shared right aligned footer')
|
|
assert(Math.abs(report.buttons[1].right - report.footer.right) < 1, 'Send at right edge')
|
|
assert(report.buttons[0].right < report.buttons[1].left && report.buttons[0].top === report.buttons[1].top, 'Cancel then send')
|
|
assert(report.scrollWidth <= report.clientWidth && report.disclosures === 0, 'No overflow or redundant disclosure')
|
|
return report
|
|
}
|
|
if (process.env.GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT) {
|
|
win.show()
|
|
const controlsOnly = process.env.GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT === 'controls'
|
|
for (const view of controlsOnly ? ['activity', 'pending'] : ['recap', 'empty', 'activity', 'pending', 'failure']) {
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + (view === 'activity' ? '?activity=1' : '?recap=1&long-summary=1' + (view === 'failure' ? '&fail-run=1' : view === 'empty' ? '&state=empty' : '')))
|
|
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
|
|
if (view === 'activity') {
|
|
await js('document.querySelector("#supervisor-tab-activity").click()')
|
|
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
|
|
} else if (view === 'pending' || view === 'failure') {
|
|
await js('document.querySelector(".supervisor-workspace__toolbar .primary-button").click()')
|
|
await wait(view === 'pending' ? '!!document.querySelector(".supervisor-workspace [role=status]")' : '!!document.querySelector(".supervisor-workspace [role=alert]")')
|
|
}
|
|
await js('document.fonts.ready')
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const width of controlsOnly ? [1440, 1024, 390] : [2000, 1440, 1024, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
await js('document.querySelector(".page-shell").scrollTop = 0')
|
|
await settle()
|
|
const report = await js(`(() => {
|
|
const box = selector => { const r = document.querySelector(selector)?.getBoundingClientRect(); return r && { left:r.left, right:r.right, width:r.width, height:r.height, top:r.top, bottom:r.bottom }; };
|
|
return { width:innerWidth, pageWidth:document.documentElement.scrollWidth,
|
|
panel:box('.heartbeat-center > [role=tabpanel]:not([hidden])'), toolbar:box('.supervisor-workspace__toolbar'), history:box('.supervisor-workspace__result-navigation'), recap:box('.supervisor-workspace__recap'),
|
|
prose:box('.supervisor-workspace__prose'), activity:box('.supervisor-activity'),
|
|
steps:box('.supervisor-activity__steps'),
|
|
empty:box('.supervisor-workspace > .empty-state'), emptyTitle:box('.supervisor-workspace > .empty-state strong'),
|
|
stageConnectors:[...document.querySelectorAll('.supervisor-activity__steps li:not(:last-child)')].map(e=>getComputedStyle(e,'::after').borderTopWidth),
|
|
summaryLength:[...document.querySelectorAll('.supervisor-workspace__summary')].map(e=>e.textContent).join('').length,
|
|
tail:document.querySelector('.supervisor-workspace__prose')?.textContent.includes('长摘要末尾'),
|
|
controls:[...document.querySelectorAll('.supervisor-activity > .supervisor-workspace__action-bar > *')].map(e=>{const r=e.getBoundingClientRect();return {left:r.left,right:r.right,top:r.top};}),
|
|
toolbarControls:[...document.querySelectorAll('.supervisor-workspace__toolbar select, .supervisor-workspace__toolbar button')].map(e=>({disabled:e.disabled,text:e.textContent,top:e.getBoundingClientRect().top,bottom:e.getBoundingClientRect().bottom})),
|
|
notice:box('.supervisor-workspace__run-status'), dismiss:box('.supervisor-workspace__run-status .icon-button'),
|
|
cancel:box('.supervisor-activity__item .danger-ghost') };
|
|
})()`)
|
|
assert(report.pageWidth <= width, 'No page overflow')
|
|
if (view === 'empty') {
|
|
assert(Math.abs(report.empty.width - report.panel.width) < 1, 'Empty state fills panel');
|
|
assert(Math.abs((report.emptyTitle.left + report.emptyTitle.right) / 2 - (report.panel.left + report.panel.right) / 2) < 1, 'Empty state title centered');
|
|
assert(report.empty.top >= report.toolbar.bottom, 'Empty state below toolbar');
|
|
} else if (view !== 'activity') {
|
|
assert(report.summaryLength > 2000 && report.tail, 'Long summary retained')
|
|
assert(Math.abs(report.recap.width - report.panel.width) < 1, 'Recap fills the page panel')
|
|
for (const control of [report.toolbar, report.history]) {
|
|
assert(Math.abs(control.left - report.recap.left) < 1 && Math.abs(control.right - report.recap.right) < 1, 'Toolbar, history and result share edges')
|
|
}
|
|
assert(report.prose.left - report.recap.left <= 25, 'No separately centered inner body')
|
|
if (view === 'pending') {
|
|
assert(report.toolbarControls.every(control => !control.disabled), 'Long review keeps toolbar enabled')
|
|
assert.equal(report.toolbarControls[2].text, '回顾当前进展', 'Start name remains stable')
|
|
assert(report.dismiss.width >= 28 && report.dismiss.height >= 28, 'Notice close target remains usable')
|
|
assert(report.dismiss.right <= report.notice.right && report.dismiss.bottom <= report.notice.bottom, 'Close remains inside notice')
|
|
assert(report.notice.right - report.dismiss.right <= 20, 'Notice close stays at the right edge on narrow screens')
|
|
if (width >= 1024) assert(Math.max(...report.toolbarControls.map(control => control.top)) - Math.min(...report.toolbarControls.map(control => control.top)) <= 4, 'Toolbar stays on one aligned row')
|
|
}
|
|
} else {
|
|
assert(Math.abs(report.activity.width - report.panel.width) < 1, 'Activity fills the page panel')
|
|
assert(report.cancel.width >= 28 && report.cancel.right <= report.panel.right, 'Cancel fits activity panel')
|
|
if (width >= 1024) {
|
|
assert(report.steps.height < 40, 'Compact stage row')
|
|
assert(report.stageConnectors.every(value => value === '1px'), 'Connected stages')
|
|
assert(report.controls[2].left - report.controls[1].right <= 24, 'Refresh grouped with filter')
|
|
}
|
|
}
|
|
reports.push({ view, theme, ...report })
|
|
await writeFile(join(artifacts, `${view}-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
if (view === 'recap') {
|
|
await js('document.querySelector(".supervisor-workspace__prose").lastElementChild.scrollIntoView({block:"end"})')
|
|
await settle()
|
|
await writeFile(join(artifacts, `${view}-tail-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert.deepEqual(errors, [])
|
|
await writeFile(join(artifacts, 'content-layout-measurements.json'), JSON.stringify(reports, null, 2))
|
|
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
|
|
win.destroy()
|
|
app.quit()
|
|
return
|
|
}
|
|
if (process.env.GOODBUDDY_SUPERVISOR_ACTIVITY) {
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?activity=1')
|
|
await wait('!!document.querySelector("#supervisor-tab-activity")')
|
|
await js('document.querySelector("#supervisor-tab-activity").click()')
|
|
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
|
|
await js('document.fonts.ready')
|
|
win.show()
|
|
win.focus()
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const width of [1440, 390]) {
|
|
win.setContentSize(width, 1000)
|
|
await js('document.querySelectorAll(".supervisor-activity__item details").forEach(e => e.open = false); document.querySelector(".page-shell").scrollTop = 0')
|
|
await settle()
|
|
const report = await js(`(() => {
|
|
const root = document.querySelector('.supervisor-activity');
|
|
const item = root.querySelector('.supervisor-activity__item');
|
|
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth, clientWidth: root.clientWidth, scrollWidth: root.scrollWidth,
|
|
itemHeight: item.getBoundingClientRect().height,
|
|
states: [...item.querySelectorAll('[data-phase]')].map(e => e.dataset.state),
|
|
diagnosticOpen: item.querySelector('.supervisor-activity__diagnostics').open,
|
|
advancedOpen: item.querySelector('.supervisor-activity__advanced').open };
|
|
})()`)
|
|
assert(report.pageWidth <= width && report.scrollWidth <= report.clientWidth, 'Activity overflow')
|
|
assert.deepEqual(report.states, ['completed', 'completed', 'failed', 'pending'])
|
|
assert(!report.diagnosticOpen && !report.advancedOpen, 'Details collapsed by default')
|
|
assert(report.itemHeight < (width === 390 ? 650 : 440), 'Compact run summary')
|
|
reports.push({ theme, ...report })
|
|
await writeFile(join(artifacts, `review-activity-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
await js('document.querySelector(".supervisor-activity__diagnostics summary").focus()')
|
|
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Enter' })
|
|
win.webContents.sendInputEvent({ type: 'char', keyCode: '\r' })
|
|
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Enter' })
|
|
await settle()
|
|
assert(await js('document.querySelector(".supervisor-activity__diagnostics").open'), 'Native keyboard disclosure')
|
|
const error = await js(`(() => { const e = document.querySelector('.supervisor-activity__diagnostics pre'); return { height: e.clientHeight, scrollHeight: e.scrollHeight, selectable: getComputedStyle(e).userSelect, text: e.textContent }; })()`)
|
|
assert(error.height <= 240 && error.scrollHeight > error.height && error.selectable === 'text' && error.text.includes('relations'), 'Full bounded selectable error')
|
|
await js('document.querySelector(".supervisor-activity__diagnostics").scrollIntoView({block:"center"})')
|
|
await settle()
|
|
await writeFile(join(artifacts, `review-error-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
await js('document.querySelector(".supervisor-activity__diagnostics").open = false; document.querySelector(".supervisor-activity__item button[aria-expanded=false]")?.click()')
|
|
await wait('!!document.querySelector(".supervisor-activity__batch")')
|
|
await js('document.querySelectorAll(".supervisor-activity__project, .supervisor-activity__conversation").forEach(e => e.open = true); document.querySelector(".supervisor-activity__tree").scrollIntoView({block:"start"})')
|
|
await settle()
|
|
assert(await js('document.querySelectorAll(".supervisor-activity__batch").length === 6'), 'All six saved leaves')
|
|
assert(await js('document.documentElement.scrollWidth <= innerWidth'), 'Hierarchy overflow')
|
|
await writeFile(join(artifacts, `review-tree-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
await js('document.querySelector(".supervisor-activity__item button[aria-expanded=true]").click()')
|
|
}
|
|
}
|
|
await js('document.querySelectorAll(".supervisor-activity__item")[1].querySelector("button[aria-expanded]").click()')
|
|
await wait('document.querySelectorAll(".supervisor-activity__batch").length === 10')
|
|
await js('[...document.querySelectorAll(".supervisor-activity__tree button")].find(e => e.textContent === "下一页").click()')
|
|
await wait('document.querySelectorAll(".supervisor-activity__batch").length === 2')
|
|
assert(await js('[...document.querySelectorAll(".supervisor-activity__tree button")].find(e => e.textContent === "下一页").disabled'), 'Paging ends at persisted batch count')
|
|
assert.deepEqual(errors, [])
|
|
await writeFile(join(artifacts, 'review-activity-measurements.json'), JSON.stringify(reports, null, 2))
|
|
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
|
|
win.destroy()
|
|
app.quit()
|
|
return
|
|
}
|
|
if (process.env.GOODBUDDY_SUPERVISOR_RECAP) {
|
|
for (const scenario of ['populated', 'automatic-empty', 'empty']) {
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?recap=1' + (scenario === 'populated' ? '&menu=1' : scenario === 'empty' ? '&state=empty' : ''))
|
|
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
|
|
await js('document.fonts.ready')
|
|
for (const width of [1440, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
for (const tab of ['overview', 'plans']) {
|
|
await js(`document.querySelector('#supervisor-tab-${tab}').click(); document.querySelector('.page-shell').scrollTop = 0`)
|
|
await settle()
|
|
const report = await js(`(() => {
|
|
const panel = document.querySelector('#supervisor-panel-${tab}');
|
|
const recap = panel.querySelector('.supervisor-workspace__recap');
|
|
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
|
|
clientWidth: panel.clientWidth, scrollWidth: panel.scrollWidth,
|
|
reports: !!panel.querySelector('#heartbeat-reports-title'),
|
|
suggestions: !!panel.querySelector('#supervision-suggestions-title'),
|
|
refresh: [...panel.querySelectorAll('button')].filter(b => b.getClientRects().length && b.textContent.includes('刷新')).length,
|
|
recap: !!recap && !!recap.getClientRects().length,
|
|
paragraphs: recap?.querySelectorAll('.supervisor-workspace__summary').length ?? 0,
|
|
proseWidth: recap?.querySelector('.supervisor-workspace__prose').getBoundingClientRect().width,
|
|
text: panel.innerText };
|
|
})()`)
|
|
assert(report.pageWidth <= width && report.scrollWidth <= report.clientWidth, 'Recap/automatic overflow')
|
|
assert.equal(report.refresh, 1, 'One refresh owner per panel')
|
|
assert.equal(report.reports, tab === 'plans' && scenario === 'populated')
|
|
assert.equal(report.suggestions, tab === 'plans' && scenario === 'populated')
|
|
assert.equal(report.recap, tab === 'overview' && scenario !== 'empty')
|
|
if (tab === 'overview') {
|
|
assert(!report.text.includes('暂无历史心跳报告') && !report.text.includes('待确认记忆') && !report.text.includes('监督建议'))
|
|
if (scenario !== 'empty') {
|
|
assert.equal(report.paragraphs, 4)
|
|
assert(report.text.includes('外部评审时间') && report.text.includes('未解决事项'))
|
|
assert(report.clientWidth - report.proseWidth <= 50, 'Prose fills the panel within card padding')
|
|
} else assert(report.text.includes('还没有成功回顾'))
|
|
} else {
|
|
assert(!report.text.includes('暂无历史心跳报告'))
|
|
assert.equal(report.text.includes('历史心跳报告'), scenario === 'populated')
|
|
}
|
|
reports.push({ scenario, tab, ...report })
|
|
const prefix = `recap-${scenario}-${tab}-${width}`
|
|
await writeFile(join(artifacts, `${prefix}.png`), (await win.webContents.capturePage()).toPNG())
|
|
if (tab === 'plans' && scenario === 'populated') {
|
|
for (const anchor of ['supervision-suggestions-title', 'heartbeat-reports-title']) {
|
|
await js(`document.getElementById('${anchor}').scrollIntoView({block:'start'})`)
|
|
if (anchor === 'heartbeat-reports-title' && scenario === 'populated') await js(`document.querySelector('#heartbeat-panel-history button[aria-expanded=false]')?.click()`)
|
|
await settle()
|
|
await writeFile(join(artifacts, `${prefix}-${anchor}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (scenario === 'populated') {
|
|
await js('document.querySelector("#supervisor-tab-overview").click()')
|
|
// Toolbar: incremental review button, refresh, and the more menu holding re-analysis.
|
|
const toolbar = await js(`(() => {
|
|
const bar = document.querySelector('.supervisor-workspace__toolbar');
|
|
const rect = bar.getBoundingClientRect();
|
|
const more = bar.querySelector('[aria-haspopup=menu]');
|
|
// Rows are groups of items whose vertical ranges overlap.
|
|
const items = [...bar.children].filter(el => el.getClientRects().length).map(el => el.getBoundingClientRect()).sort((a, b) => a.top - b.top);
|
|
const tops = items.reduce((rows, rect) => { const last = rows.at(-1); if (last && rect.top < last.bottom) last.bottom = Math.max(last.bottom, rect.bottom); else rows.push({ top: Math.round(rect.top), bottom: rect.bottom }); return rows }, []).map(row => row.top);
|
|
return { primary: bar.querySelector('.primary-button').textContent, more: more?.getAttribute('aria-label'),
|
|
overflow: bar.scrollWidth > rect.width + 1, rows: new Set(tops).size };
|
|
})()`)
|
|
assert.equal(toolbar.primary, '回顾')
|
|
assert.equal(toolbar.more, '更多回顾操作')
|
|
assert.equal(toolbar.overflow, false, 'Recap toolbar overflows')
|
|
assert(toolbar.rows <= 2, `Recap toolbar wraps into ${toolbar.rows} rows`)
|
|
await js('document.querySelector(".supervisor-workspace__toolbar [aria-haspopup=menu]").click()')
|
|
await wait('!!document.querySelector("[role=menu] [role=menuitem]")')
|
|
assert.equal(await js('document.querySelector("[role=menu] [role=menuitem]").textContent'), '重新整理…')
|
|
await js('document.querySelector("[role=menu] [role=menuitem]").click()')
|
|
await wait('!!document.querySelector(".supervisor-workspace__confirm")')
|
|
const confirm = await js(`(() => { const el = document.querySelector('.supervisor-workspace__confirm'); const r = el.getBoundingClientRect();
|
|
return { width: r.width, overflow: el.scrollWidth > el.clientWidth + 1, text: el.textContent, buttons: [...el.querySelectorAll('button')].map(b => b.textContent) } })()`)
|
|
assert.equal(confirm.overflow, false, 'Re-analysis confirmation overflows')
|
|
assert.deepEqual(confirm.buttons, ['取消', '重新整理'])
|
|
assert(confirm.text.includes('可能产生较多模型用量'))
|
|
await settle()
|
|
await writeFile(join(artifacts, `recap-reanalyze-confirm-${win.getContentSize()[0]}.png`), (await win.webContents.capturePage()).toPNG())
|
|
await js('document.querySelector(".supervisor-workspace__confirm .secondary-button").click()')
|
|
await wait('!document.querySelector(".supervisor-workspace__confirm")')
|
|
await js(`(() => { const s = document.querySelector('.supervisor-workspace__result-navigation select'); s.value = 'older-result'; s.dispatchEvent(new Event('change', {bubbles:true})); })()`)
|
|
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
|
|
await js('document.querySelector(".supervisor-workspace__toolbar .primary-button").click(); document.querySelector(".page-shell").scrollTop = 0')
|
|
await settle()
|
|
assert(await js('document.querySelector(".supervisor-workspace__recap").textContent.includes("上期结果")'))
|
|
assert(await js('document.querySelector(".supervisor-workspace [role=status]").textContent.includes("新回顾正在整理")'))
|
|
await writeFile(join(artifacts, 'recap-running-history-390.png'), (await win.webContents.capturePage()).toPNG())
|
|
await js('document.querySelector(".supervisor-workspace [role=status] button").click()')
|
|
await wait('document.querySelector("#supervisor-tab-activity").getAttribute("aria-selected") === "true"')
|
|
}
|
|
}
|
|
assert.deepEqual(errors, [])
|
|
await writeFile(join(artifacts, 'recap-measurements.json'), JSON.stringify(reports, null, 2))
|
|
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
|
|
win.destroy()
|
|
app.quit()
|
|
return
|
|
}
|
|
if (process.env.GOODBUDDY_SUPERVISOR_AUTOMATIC_OVERVIEW || process.env.GOODBUDDY_SUPERVISOR_DISCUSSION) {
|
|
for (const scenario of process.env.GOODBUDDY_SUPERVISOR_DISCUSSION ? [] : ['populated', 'empty', 'plan-only']) {
|
|
const empty = scenario !== 'populated'
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + (scenario === 'populated' ? '?menu=1' : scenario === 'plan-only' ? '?plan-only=1' : ''))
|
|
await wait('!!document.querySelector("#supervisor-tab-plans")')
|
|
await js('document.fonts.ready')
|
|
await js('document.documentElement.dataset.theme = "light"')
|
|
for (const width of [1440, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
for (const tab of ['plans', 'activity']) {
|
|
await js(`document.querySelector('#supervisor-tab-${tab}').click(); document.querySelector('.page-shell').scrollTop = 0`)
|
|
if (tab === 'activity') await wait('document.querySelector(".supervisor-activity")?.getAttribute("aria-busy") === "false"')
|
|
await settle()
|
|
const report = await js(`(() => {
|
|
const panel = document.querySelector('#supervisor-panel-${tab}');
|
|
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
|
|
panelWidth: panel.clientWidth, scrollWidth: panel.scrollWidth,
|
|
overview: !!panel.querySelector('#heartbeat-panel-overview'),
|
|
metrics: !!panel.querySelector('.heartbeat-center__metrics'),
|
|
trend: !!panel.querySelector('#heartbeat-trend-title'),
|
|
audit: !!panel.querySelector('#heartbeat-runs-title'),
|
|
reports: !!panel.querySelector('#heartbeat-reports-title'),
|
|
suggestions: !!panel.querySelector('#supervision-suggestions-title'),
|
|
suggestionItems: panel.querySelectorAll('.supervision-suggestions__list > li').length,
|
|
emptyReports: panel.textContent.includes('暂无历史心跳报告'),
|
|
plans: !!panel.querySelector('#heartbeat-panel-plans'),
|
|
create: [...panel.querySelectorAll('button')].filter(b => b.textContent === '创建心跳计划').length,
|
|
emptyStates: panel.querySelectorAll('#heartbeat-panel-overview .empty-state').length,
|
|
activity: panel.querySelectorAll('.supervisor-activity__item').length };
|
|
})()`)
|
|
assert(report.pageWidth <= width && report.scrollWidth <= report.panelWidth, 'Automatic panel overflow')
|
|
for (const key of ['overview', 'plans']) assert.equal(report[key], tab === 'plans', key)
|
|
// Statistics, the trend and the run audit are gone; earlier reports stay readable.
|
|
for (const key of ['metrics', 'trend', 'audit']) assert.equal(report[key], false, key)
|
|
for (const key of ['reports', 'suggestions']) assert.equal(report[key], tab === 'plans' && !empty, key)
|
|
assert.equal(report.suggestionItems, tab === 'plans' && !empty ? 2 : 0)
|
|
assert.equal(report.emptyReports, false)
|
|
assert.equal(report.create, tab === 'plans' ? 1 : 0)
|
|
assert.equal(report.emptyStates, 0)
|
|
if (tab === 'activity') assert.equal(report.activity, 3)
|
|
reports.push({ scenario, empty, tab, ...report })
|
|
const prefix = `automatic-${scenario}-${tab}-light-${width}`
|
|
await writeFile(join(artifacts, `${prefix}.png`), (await win.webContents.capturePage()).toPNG())
|
|
if (tab === 'plans') {
|
|
for (const anchor of empty ? [] : ['supervision-suggestions-title', 'heartbeat-reports-title']) {
|
|
await js(`document.getElementById('${anchor}').scrollIntoView({block:'start'})`)
|
|
await settle()
|
|
await writeFile(join(artifacts, `${prefix}-${anchor}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
await js('document.querySelector(".heartbeat-settings__intro button").click()')
|
|
await wait('!!document.querySelector("[role=dialog]")')
|
|
assert(await js('document.querySelector("[role=dialog] > header button").getAttribute("aria-label") === "关闭心跳计划"'))
|
|
assert.equal(await js('document.querySelector("[role=dialog] > footer").textContent'), '取消保存并启用计划')
|
|
const intervention = await js(`(() => {
|
|
const dialog = document.querySelector('[role=dialog]');
|
|
const group = [...dialog.querySelectorAll('fieldset')].find(f => f.textContent.includes('介入方式'));
|
|
const buttons = group ? [...group.querySelectorAll('button')].filter(b => b.textContent === '生成建议' || b.textContent === '仅更新记忆') : [];
|
|
const body = dialog.querySelector('.custom-task-dialog__body, [class*=body]') || dialog;
|
|
return { present: !!group, options: buttons.map(b => b.textContent), pressed: buttons.find(b => b.getAttribute('aria-pressed') === 'true' || b.getAttribute('aria-checked') === 'true')?.textContent,
|
|
overflow: body.scrollWidth > body.clientWidth + 1 };
|
|
})()`)
|
|
assert.equal(intervention.present, true, 'Intervention field missing')
|
|
assert.deepEqual(intervention.options, ['生成建议', '仅更新记忆'])
|
|
assert.equal(intervention.overflow, false, 'Plan modal overflows horizontally')
|
|
if (width === 390) await writeFile(join(artifacts, `plan-modal-intervention-${scenario}-390.png`), (await win.webContents.capturePage()).toPNG())
|
|
await js('document.querySelector("[role=dialog] > header button").click()')
|
|
await wait('!document.querySelector("[role=dialog]")')
|
|
}
|
|
}
|
|
}
|
|
if (!empty) {
|
|
await js('document.querySelector("#supervisor-tab-plans").click()')
|
|
await js('[...document.querySelectorAll(".heartbeat-settings__actions button")].find(b => b.textContent === "执行记录").click()')
|
|
await wait('document.querySelector(".supervisor-activity select")?.value === "plan" && document.querySelectorAll(".supervisor-activity__item").length === 2')
|
|
assert(await js('!document.querySelector("#heartbeat-panel-overview, #heartbeat-runs-title")'))
|
|
await js('[...document.querySelectorAll(".supervisor-activity button")].find(b => b.textContent === "清除筛选").click()')
|
|
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
|
|
}
|
|
}
|
|
assert.deepEqual(errors, [])
|
|
await writeFile(join(artifacts, 'automatic-overview-measurements.json'), JSON.stringify(reports, null, 2))
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?preview=1')
|
|
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
|
|
await js('document.querySelector("#supervisor-tab-graph").click()')
|
|
await wait('!!document.querySelector(".supervisor-workspace__map")')
|
|
await js(`document.querySelector('.supervisor-workspace__map [aria-label="观察工作过程"]').dispatchEvent(new MouseEvent('click', {bubbles:true}))`)
|
|
await settle()
|
|
await js('document.querySelector(".supervisor-workspace__detail > .link-button").click()')
|
|
await wait('!!document.querySelector(".supervisor-discussion > button")')
|
|
await js('document.querySelector(".supervisor-discussion > button").click()')
|
|
await wait('!!document.querySelector(".supervisor-discussion textarea")')
|
|
await js('document.fonts.ready')
|
|
const previews = []
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const width of [1440, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
await js(`document.querySelector('.supervisor-discussion').scrollIntoView({block:'center'})`)
|
|
await settle()
|
|
const report = await js(`(() => {
|
|
const preview = document.querySelector('.supervisor-discussion');
|
|
const box = e => { const r = e.getBoundingClientRect(); return {left:r.left, right:r.right, top:r.top, bottom:r.bottom}; };
|
|
return { width:innerWidth, pageWidth:document.documentElement.scrollWidth,
|
|
clientWidth:preview.clientWidth, scrollWidth:preview.scrollWidth,
|
|
question:preview.querySelector('textarea').value, text:preview.textContent,
|
|
controls:[...preview.querySelectorAll('textarea, button')].map(box) };
|
|
})()`)
|
|
assert(report.pageWidth <= width && report.scrollWidth <= report.clientWidth, 'Discussion preview overflow')
|
|
assert(report.question.trim() && report.text.includes('模拟会议记录'), 'Discussion question and destination visible')
|
|
assert(report.controls.every(box => box.left >= 0 && box.right <= width && box.top >= 0 && box.bottom <= 1100), 'Preview controls fit viewport')
|
|
assert(await js('!document.querySelector(".supervisor-discussion [role=alert]")'), 'Preview loads without error')
|
|
assert(await js('!document.querySelector(".supervisor-discussion details")'), 'No redundant context disclosure')
|
|
previews.push({ theme, ...report, editor: await checkDiscussion() })
|
|
await writeFile(join(artifacts, `discussion-preview-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
}
|
|
await js('document.querySelector(".supervisor-discussion .secondary-button").click()')
|
|
await wait('!document.querySelector(".supervisor-discussion textarea")')
|
|
assert.deepEqual(errors, [])
|
|
await writeFile(join(artifacts, 'discussion-preview-measurements.json'), JSON.stringify(previews, null, 2))
|
|
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
|
|
win.destroy()
|
|
app.quit()
|
|
return
|
|
}
|
|
if (process.env.GOODBUDDY_SUPERVISOR_SIDEBAR) {
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL)
|
|
await wait('!!document.querySelector(".supervision-card")')
|
|
await js('document.fonts.ready')
|
|
win.show()
|
|
win.focus()
|
|
await wait('[...document.querySelectorAll(".supervision-card button")].some(b => b.textContent === "继续讨论" && !b.disabled)')
|
|
await js('[...document.querySelectorAll(".supervision-card button")].find(b => b.textContent === "继续讨论").click()')
|
|
await wait('!!document.querySelector(".supervision-discussion-editor textarea")')
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const width of [480, 300, 200]) {
|
|
win.setContentSize(width, 640)
|
|
await js('document.querySelector(".supervision-card__header button").focus()')
|
|
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab' })
|
|
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Tab' })
|
|
await settle()
|
|
const report = await js(`(() => {
|
|
const card = document.querySelector('.supervision-card');
|
|
const header = card.querySelector('.supervision-card__header');
|
|
const buttons = [...header.querySelectorAll('button')];
|
|
return {
|
|
width: innerWidth, scrollWidth: document.documentElement.scrollWidth,
|
|
cardWidth: card.clientWidth, cardScrollWidth: card.scrollWidth,
|
|
headerHeight: header.getBoundingClientRect().height,
|
|
buttons: buttons.map(b => ({ width: b.getBoundingClientRect().width,
|
|
height: b.getBoundingClientRect().height, background: getComputedStyle(b).backgroundColor })),
|
|
focus: getComputedStyle(document.activeElement).outlineStyle,
|
|
text: card.textContent
|
|
};
|
|
})()`)
|
|
assert(report.scrollWidth <= report.width, `Page overflow: ${JSON.stringify(report)}`)
|
|
assert(report.cardScrollWidth <= report.cardWidth, 'Card overflow')
|
|
assert.equal(report.headerHeight, 34)
|
|
assert(report.buttons.every(b => b.width === 34 && b.height === 34), 'Compact shared buttons')
|
|
assert.equal(report.focus, 'solid')
|
|
assert(!report.text.includes('simulated-conversation-uuid'), 'Raw UUID visible')
|
|
reports.push({ theme, ...report, editor: await checkDiscussion() })
|
|
await js('document.querySelector(".supervision-discussion-editor footer").scrollIntoView({block:"end"})')
|
|
await settle()
|
|
assert(await js(`([...document.querySelectorAll('.supervision-discussion-editor footer button')].every(e => {
|
|
const r = e.getBoundingClientRect();
|
|
return r.top >= 0 && r.bottom <= innerHeight && e.contains(document.elementFromPoint(r.x+r.width/2, r.y+r.height/2));
|
|
}))`), 'Sidebar footer reachable and hittable after scrolling')
|
|
await writeFile(join(artifacts, `sidebar-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
}
|
|
assert.deepEqual(errors, [])
|
|
await writeFile(join(artifacts, 'sidebar-measurements.json'), JSON.stringify(reports, null, 2))
|
|
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
|
|
win.destroy()
|
|
app.quit()
|
|
return
|
|
}
|
|
if (process.env.GOODBUDDY_SUPERVISOR_PLAN_MODAL) {
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?menu=1')
|
|
await wait('!!document.querySelector("#supervisor-tab-plans")')
|
|
await js('document.fonts.ready')
|
|
await js('document.querySelector("#supervisor-tab-plans").click()')
|
|
assert.equal(await js('document.querySelectorAll("[role=tab]").length'), 5)
|
|
win.show()
|
|
win.focus()
|
|
for (const mode of ['create', 'edit']) {
|
|
const opener = mode === 'create' ? '.heartbeat-settings__intro button' : '.heartbeat-settings__actions button[aria-label^="编辑"]'
|
|
await js(`document.querySelector('${opener}').focus(); document.querySelector('${opener}').click()`)
|
|
await wait('!!document.querySelector("[role=dialog]")')
|
|
assert(await js('document.activeElement === document.querySelector("[role=dialog] input")'), 'Name initial focus')
|
|
await js(`(() => {
|
|
const select = document.querySelector('[aria-label="监督频率"]');
|
|
select.value = 'weekly'; select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
})()`)
|
|
await wait('!!document.querySelector("[aria-label=监督星期]")')
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const [width, height] of [[1440, 1100], [390, 1100], [390, 480]]) {
|
|
win.setContentSize(width, height)
|
|
await settle()
|
|
const measure = () => js(`(() => {
|
|
const dialog = document.querySelector('[role=dialog]');
|
|
const header = dialog.querySelector(':scope > header'), footer = dialog.querySelector(':scope > footer');
|
|
const body = dialog.querySelector(':scope > .custom-task-dialog__content');
|
|
const box = e => { const r = e.getBoundingClientRect(); return { left:r.left, right:r.right, top:r.top, bottom:r.bottom, width:r.width, height:r.height }; };
|
|
const close = header.querySelector('button'), buttons = [...footer.querySelectorAll('button')];
|
|
const hit = e => { const r = e.getBoundingClientRect(); return e.contains(document.elementFromPoint(r.x+r.width/2, r.y+r.height/2)); };
|
|
return { width:innerWidth, height:innerHeight, pageWidth:document.documentElement.scrollWidth,
|
|
dialog:box(dialog), header:box(header), title:box(header.querySelector('h2')), close:box(close),
|
|
footer:box(footer), buttons:buttons.map(box), hit:[close,...buttons].every(hit),
|
|
headerButtons:header.querySelectorAll('button').length, closeLabel:close.getAttribute('aria-label'),
|
|
tooltip:close.title, bodyWidth:body.clientWidth, bodyScrollWidth:body.scrollWidth,
|
|
bodyHeight:body.clientHeight, bodyScrollHeight:body.scrollHeight, bodyTop:body.scrollTop,
|
|
footerAlign:getComputedStyle(footer).justifyContent, text:buttons.map(e=>e.textContent) };
|
|
})()`)
|
|
const before = await measure()
|
|
assert(before.pageWidth <= width && before.bodyScrollWidth <= before.bodyWidth, 'No horizontal overflow')
|
|
assert(before.dialog.top >= 0 && before.dialog.bottom <= height, 'Dialog fits viewport')
|
|
assert(before.headerButtons === 1 && before.closeLabel === '关闭心跳计划' && before.tooltip === before.closeLabel, 'Accessible header X only')
|
|
assert(before.title.right <= before.close.left && before.close.right > (before.dialog.left + before.dialog.right) / 2, 'Title left, close right')
|
|
assert.equal(before.footerAlign, 'flex-end')
|
|
assert.equal(before.text[0], '取消')
|
|
assert(before.buttons[0].right <= before.buttons[1].left && before.buttons[0].top === before.buttons[1].top, 'Cancel then save on same row')
|
|
assert(before.buttons[1].right > (before.dialog.left + before.dialog.right) / 2 && before.hit, 'Footer right and controls hittable')
|
|
await js('document.querySelector("[role=dialog] > .custom-task-dialog__content").scrollTop = 10000')
|
|
await settle()
|
|
const after = await measure()
|
|
assert.deepEqual(after.header, before.header, 'Header stays visible when body scrolls')
|
|
assert.deepEqual(after.footer, before.footer, 'Footer stays visible when body scrolls')
|
|
assert(after.hit, 'Scrolled controls hittable')
|
|
if (height === 480) assert(after.bodyTop > 0 && after.bodyScrollHeight > after.bodyHeight, 'Short body scrolls')
|
|
reports.push({ mode, theme, before, after })
|
|
await writeFile(join(artifacts, `plan-${mode}-${theme}-${width}x${height}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
}
|
|
win.focus()
|
|
win.webContents.focus()
|
|
await js('document.querySelector("[role=dialog] > header button").focus()')
|
|
const key = async (keyCode, modifiers = []) => {
|
|
win.webContents.sendInputEvent({ type: 'keyDown', keyCode, modifiers })
|
|
if (keyCode === 'Enter') win.webContents.sendInputEvent({ type: 'char', keyCode: '\r' })
|
|
win.webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers })
|
|
await settle()
|
|
}
|
|
await key('Tab', ['shift'])
|
|
assert(await js('document.activeElement.matches("footer .primary-button")'), 'Shift Tab wraps to save')
|
|
await key('Tab', ['shift'])
|
|
assert(await js('document.activeElement.matches("footer .secondary-button")'), 'Footer cancel keyboard reachable')
|
|
const focus = await js('({ focused: document.hasFocus(), visible: document.activeElement.matches(":focus-visible"), outline: getComputedStyle(document.activeElement).outlineStyle })')
|
|
assert(focus.visible && focus.outline === 'solid', `Visible keyboard focus: ${JSON.stringify(focus)}`)
|
|
await key('Enter')
|
|
await wait('document.querySelector("[role=dialog]").textContent.includes("放弃未保存")')
|
|
assert(await js('document.activeElement.textContent === "继续编辑"'), 'Confirmation initial focus')
|
|
await key('Escape')
|
|
assert(await js('document.activeElement === document.querySelector("[role=dialog] input")'), 'Escape returns to editing')
|
|
await js('document.querySelector("[role=dialog] > header button").click()')
|
|
await wait('!!document.querySelector("[role=dialog] .danger-solid")')
|
|
await js('document.querySelector("[role=dialog] .danger-solid").click()')
|
|
await wait('!document.querySelector("[role=dialog]")')
|
|
assert(await js(`document.activeElement === document.querySelector('${opener}')`), 'Opener focus restored')
|
|
}
|
|
assert.deepEqual(errors, [])
|
|
await writeFile(join(artifacts, 'plan-modal-measurements.json'), JSON.stringify(reports, null, 2))
|
|
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
|
|
win.destroy()
|
|
app.quit()
|
|
return
|
|
}
|
|
await open(false)
|
|
await js(
|
|
'document.querySelector(".supervisor-workspace__canvas-tools .link-button").click()'
|
|
)
|
|
await wait(
|
|
'!document.querySelector(".supervisor-workspace__map.has-selection")'
|
|
)
|
|
assert(await js(`(() => {
|
|
const ring = document.querySelector('.supervisor-workspace__timeline-ring');
|
|
const start = ring.getPointAtLength(0), next = ring.getPointAtLength(20), end = ring.getPointAtLength(ring.getTotalLength());
|
|
return next.x > start.x && next.y < start.y && end.x < start.x && end.y > 490;
|
|
})()`), 'Time must run counter-clockwise and leave a bottom gap')
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const width of [1440, 1024, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
await settle()
|
|
const report = await js(`(() => {
|
|
const q = s => document.querySelector('.supervisor-workspace' + s);
|
|
const box = e => { const r = e.getBoundingClientRect(); return { width: r.width, height: r.height, top: r.top, bottom: r.bottom, clientWidth: e.clientWidth, scrollWidth: e.scrollWidth }; };
|
|
const svg = q('__map'), scroller = q('__map-scroll');
|
|
const texts = [...svg.querySelectorAll('text')].map(e => {
|
|
const b = e.getBBox(), c = getComputedStyle(e), r = e.getBoundingClientRect();
|
|
return { text: e.textContent, width: b.width, height: b.height, fontSize: c.fontSize,
|
|
screenFontSize: parseFloat(c.fontSize) * e.getScreenCTM().a, fill: c.fill,
|
|
inSvg: b.x >= 0 && b.y >= 0 && b.x + b.width <= 760 && b.y + b.height <= 620,
|
|
painted: c.display !== 'none' && c.visibility === 'visible' && c.fill !== 'none' && Number(c.opacity) > 0 && r.width > 0,
|
|
x: b.x, y: b.y };
|
|
});
|
|
const tokens = Object.fromEntries(${process.env.GOODBUDDY_SUPERVISOR_TOKENS}.map(t => [t, getComputedStyle(q('')).getPropertyValue(t).trim()]));
|
|
return { viewport: innerWidth, page: box(document.documentElement), shell: box(document.querySelector('.page-shell')),
|
|
container: box(q('')), grid: getComputedStyle(q('__graph-layout')).gridTemplateColumns,
|
|
list: box(q('__graph-list')), canvas: box(q('__graph-canvas')), detail: box(q('__detail')), svg: box(svg), scroller: box(scroller), texts, tokens };
|
|
})()`)
|
|
reports.push({ theme, ...report })
|
|
await writeFile(
|
|
join(artifacts, `${theme}-${width}.png`),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
assert(report.page.scrollWidth <= width, JSON.stringify(report))
|
|
if (width >= 1024) {
|
|
assert(Math.abs(report.list.top - report.canvas.top) < 1, 'Sidebar top alignment')
|
|
assert(Math.abs(report.list.bottom - report.canvas.bottom) < 1,
|
|
`Sidebar must fill canvas height: ${JSON.stringify({ list: report.list, canvas: report.canvas })}`)
|
|
if (width === 1440)
|
|
assert(Math.abs(report.list.bottom - report.detail.bottom) < 1, 'Sidebar/detail height alignment')
|
|
}
|
|
assert(
|
|
report.shell.scrollWidth <= report.shell.clientWidth,
|
|
'Shell overflow'
|
|
)
|
|
assert(
|
|
report.container.scrollWidth <= report.container.clientWidth,
|
|
'Container overflow'
|
|
)
|
|
assert(
|
|
report.texts.every(
|
|
(text) => text.inSvg && text.painted && text.screenFontSize >= 11
|
|
),
|
|
`Clipped or invisible SVG text at ${width}: ${JSON.stringify(report.texts.filter(text => !(text.inSvg && text.painted && text.screenFontSize >= 11)).slice(0, 3))} svg=${JSON.stringify(report.svg)}`
|
|
)
|
|
for (const [index, a] of report.texts.entries()) {
|
|
for (const b of report.texts.slice(index + 1)) {
|
|
assert(
|
|
!(
|
|
a.x < b.x + b.width &&
|
|
a.x + a.width > b.x &&
|
|
a.y < b.y + b.height &&
|
|
a.y + a.height > b.y
|
|
),
|
|
`Overlapping labels: ${a.text} / ${b.text}`
|
|
)
|
|
}
|
|
}
|
|
assert(
|
|
Object.values(report.tokens).every(Boolean),
|
|
'Unresolved CSS variable'
|
|
)
|
|
assert(
|
|
report.texts.some((text) => text.text === '工作过程'),
|
|
'Missing real entity label'
|
|
)
|
|
if (width === 1440)
|
|
assert(
|
|
report.canvas.width > report.list.width + report.detail.width,
|
|
'Canvas must take priority'
|
|
)
|
|
if (width === 1440) {
|
|
await js('document.querySelector(".supervisor-workspace__graph-layout").scrollIntoView({block:"end"})')
|
|
await settle()
|
|
await writeFile(join(artifacts, `${theme}-1440-aligned-bottom.png`), (await win.webContents.capturePage()).toPNG())
|
|
await js('document.querySelector(".page-shell").scrollTop = 0')
|
|
}
|
|
if (width >= 1024)
|
|
assert.equal(
|
|
report.scroller.scrollWidth,
|
|
report.scroller.clientWidth,
|
|
'Desktop canvas must fit'
|
|
)
|
|
if (width === 390) {
|
|
assert(
|
|
report.scroller.scrollWidth > report.scroller.clientWidth,
|
|
'Keep mobile labels readable with local scrolling'
|
|
)
|
|
await js(
|
|
'document.querySelector(".supervisor-workspace__map-scroll").scrollLeft = 600'
|
|
)
|
|
assert(
|
|
await js(
|
|
'document.querySelector(".supervisor-workspace__map-scroll").scrollLeft > 0'
|
|
)
|
|
)
|
|
await writeFile(
|
|
join(artifacts, `${theme}-${width}-scrolled.png`),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
// The viewport stays wide while the actual workspace is constrained.
|
|
win.setContentSize(1440, 1100)
|
|
await js(
|
|
`document.documentElement.dataset.theme = 'light'; document.querySelector('.supervisor-workspace__map [aria-label="观察工作过程"]').dispatchEvent(new MouseEvent('click', {bubbles:true}))`
|
|
)
|
|
await settle()
|
|
await js(
|
|
'document.querySelector(".supervisor-workspace__detail > .link-button").click()'
|
|
)
|
|
await wait('!!document.querySelector(".supervisor-workspace__source")')
|
|
assert(
|
|
await js(
|
|
'document.querySelector(".supervisor-workspace__source").textContent.includes("不是真实用户数据")'
|
|
)
|
|
)
|
|
await settle()
|
|
await writeFile(
|
|
join(artifacts, 'selected-source-1440.png'),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
win.setContentSize(390, 1100)
|
|
await js(
|
|
'document.querySelector(".supervisor-workspace__detail").scrollIntoView({block:"start"})'
|
|
)
|
|
await settle()
|
|
await writeFile(
|
|
join(artifacts, 'selected-source-390.png'),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
assert(
|
|
await js('document.documentElement.scrollWidth <= innerWidth'),
|
|
'Mobile source overflow'
|
|
)
|
|
win.setContentSize(1440, 1100)
|
|
for (const width of [800, 860, 861, 980, 1000, 1010, 1110, 1190, 1191]) {
|
|
await js(
|
|
`document.querySelector('.supervisor-workspace').style.width = '${width}px'`
|
|
)
|
|
await settle()
|
|
const result = await js(
|
|
`(() => { const e = document.querySelector('.supervisor-workspace'); return { width: e.clientWidth, scrollWidth: e.scrollWidth, columns: getComputedStyle(e.querySelector('.supervisor-workspace__graph-layout')).gridTemplateColumns }; })()`
|
|
)
|
|
assert.equal(result.width, result.scrollWidth, 'Breakpoint overflow')
|
|
reports.push({ constrainedContainer: result })
|
|
}
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?short=1')
|
|
await wait('!!document.querySelector("#supervisor-tab-graph")')
|
|
await js('document.querySelector("#supervisor-tab-graph").click()')
|
|
await wait('!!document.querySelector(".supervisor-workspace__map")')
|
|
for (const width of [1440, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
await settle()
|
|
const short = await js(`(() => {
|
|
const list = document.querySelector('.supervisor-workspace__graph-list');
|
|
const r = list.getBoundingClientRect();
|
|
return { width: innerWidth, height: r.height, clientHeight: list.clientHeight, scrollHeight: list.scrollHeight,
|
|
canvasHeight: document.querySelector('.supervisor-workspace__graph-canvas').getBoundingClientRect().height,
|
|
buttonHeights: [...list.querySelectorAll('button')].map(e => e.getBoundingClientRect().height) };
|
|
})()`)
|
|
assert(short.buttonHeights.every(height => height < 80), 'Short rows must not stretch to fill space')
|
|
assert.equal(short.scrollHeight, short.clientHeight, 'Short list must not acquire artificial overflow')
|
|
if (width === 1440) assert(Math.abs(short.height - short.canvasHeight) < 1, 'Short sidebar must still fill its row')
|
|
else assert(short.height <= 320, 'Short mobile list must use its natural height')
|
|
reports.push({ shortList: short })
|
|
await js('document.querySelector(".supervisor-workspace__graph-list").scrollIntoView({block:"end"})')
|
|
await settle()
|
|
await writeFile(join(artifacts, `short-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
win.setContentSize(1440, 1100)
|
|
await open(true)
|
|
for (const width of [1440, 1024, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
await settle()
|
|
const scrolling = await js(`(() => {
|
|
const sidebar = document.querySelector('.supervisor-workspace__graph-list');
|
|
const list = sidebar.querySelector('[role=tabpanel]');
|
|
const canvas = document.querySelector('.supervisor-workspace__graph-canvas');
|
|
const shell = document.querySelector('.page-shell');
|
|
list.scrollIntoView({block:'end'});
|
|
const pageHeight = shell.scrollHeight, pageTop = shell.scrollTop;
|
|
list.scrollTop = list.scrollHeight;
|
|
const last = list.lastElementChild.getBoundingClientRect();
|
|
const button = list.querySelector('button:last-of-type');
|
|
const b = button.getBoundingClientRect(), r = list.getBoundingClientRect();
|
|
return { width: innerWidth, height: sidebar.getBoundingClientRect().height, canvasHeight: canvas.getBoundingClientRect().height,
|
|
clientHeight: list.clientHeight, scrollHeight: list.scrollHeight, scrollTop: list.scrollTop,
|
|
bottomGap: r.bottom - last.bottom,
|
|
paddingBottom: parseFloat(getComputedStyle(list).paddingBottom),
|
|
bottomVisible: last.bottom <= r.bottom && last.top >= r.top,
|
|
lastButtonHit: button.contains(document.elementFromPoint(b.x + b.width / 2, b.y + b.height / 2)),
|
|
pageStable: pageHeight === shell.scrollHeight && pageTop === shell.scrollTop,
|
|
pageHeight };
|
|
})()`)
|
|
assert(scrolling.scrollHeight > scrolling.clientHeight, 'Dense list must scroll locally')
|
|
assert(Math.abs(scrolling.scrollTop + scrolling.clientHeight - scrolling.scrollHeight) <= 1, 'List bottom unreachable')
|
|
assert(scrolling.bottomVisible && scrolling.lastButtonHit, 'Last relation and trailing copy must be visible')
|
|
assert(Math.abs(scrolling.bottomGap - scrolling.paddingBottom) <= 1,
|
|
`List must use the full scroll area, leaving only its padding: ${JSON.stringify(scrolling)}`)
|
|
assert(scrolling.pageStable, 'List scrolling must not move or grow the page')
|
|
if (width >= 1024)
|
|
assert(Math.abs(scrolling.height - scrolling.canvasHeight) < 1, 'Dense list must follow canvas height')
|
|
else
|
|
assert(scrolling.height <= 320, 'Single-column list must remain bounded')
|
|
reports.push({ denseScrolling: scrolling })
|
|
await settle()
|
|
await writeFile(join(artifacts, `dense-bottom-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
// The graph row fills the window down to a steady bottom margin; each column scrolls on its own.
|
|
for (const [width, height] of [[1440, 1200], [1440, 900], [1440, 700], [1024, 1100]]) {
|
|
win.setContentSize(width, height)
|
|
await settle()
|
|
const fill = await js(`(() => {
|
|
const shell = document.querySelector('.page-shell');
|
|
shell.scrollTop = 0;
|
|
const layout = document.querySelector('.supervisor-workspace__graph-layout');
|
|
const shellBox = shell.getBoundingClientRect(), box = layout.getBoundingClientRect();
|
|
const columns = [...layout.children].map(column => {
|
|
const scroller = column.matches('.supervisor-workspace__graph-list') ? column.querySelector('[role=tabpanel]') : column;
|
|
const r = column.getBoundingClientRect();
|
|
return { name: column.className.split(' ')[0], canvas: column.matches('.supervisor-workspace__graph-canvas'), top: r.top, bottom: r.bottom,
|
|
scrolls: getComputedStyle(scroller).overflowY, clientHeight: scroller.clientHeight, scrollHeight: scroller.scrollHeight };
|
|
});
|
|
const canvas = layout.querySelector('.supervisor-workspace__graph-canvas'), map = layout.querySelector('.supervisor-workspace__map').getBoundingClientRect();
|
|
const playback = layout.querySelector('.supervisor-workspace__playback')?.getBoundingClientRect();
|
|
const graphArea = { map: { top: map.top, bottom: map.bottom, height: map.height }, playbackBottom: playback?.bottom, canvasBottom: canvas.getBoundingClientRect().bottom };
|
|
// Bottom of the first grid row (the side-by-side columns), measured from the window bottom.
|
|
const rowBottom = Math.max(...columns.filter(column => Math.abs(column.top - columns[0].top) < 1).map(column => column.bottom));
|
|
return { bottomGap: shellBox.bottom - rowBottom, wrapped: columns.length - columns.filter(column => Math.abs(column.top - columns[0].top) < 1).length,
|
|
height: box.height, pageScroll: shell.scrollHeight - shell.clientHeight, columns, graphArea };
|
|
})()`)
|
|
reports.push({ fill: { width, height, ...fill } })
|
|
await writeFile(join(artifacts, `fill-${width}x${height}.json`), JSON.stringify({ width, height, bottomGap: Math.round(fill.bottomGap), wrapped: fill.wrapped, pageScroll: fill.pageScroll, rowHeight: Math.round(fill.columns[0].bottom - fill.columns[0].top), mapHeight: Math.round(fill.graphArea.map.height) }))
|
|
// At the 660px minimum (labels stay ≥ 11px) a short window scrolls the page; otherwise the row ends at the bottom margin.
|
|
const atMinimum = Math.abs(fill.columns[0].bottom - fill.columns[0].top - 660) < 1
|
|
if (!atMinimum) {
|
|
assert(fill.bottomGap >= 20 && fill.bottomGap <= 40, `Graph must stop above the window bottom: ${JSON.stringify(fill)}`)
|
|
// Three columns fit the window with no page scroll; at two columns only the wrapped detail extends the page.
|
|
if (!fill.wrapped) assert(fill.pageScroll <= 1, `Page must not scroll at three columns: ${JSON.stringify(fill)}`)
|
|
}
|
|
const sideBySide = fill.columns.filter(column => Math.abs(column.top - fill.columns[0].top) < 1)
|
|
for (const column of sideBySide) {
|
|
assert(Math.abs(column.bottom - sideBySide[0].bottom) < 1, `Columns must share the row height: ${JSON.stringify(fill)}`)
|
|
// The graph column has no scroll bar: the graph takes the height left after the heading and event bar.
|
|
if (column.canvas) assert(column.scrollHeight <= column.clientHeight + 1 && column.scrolls === 'hidden', `Graph column must not scroll: ${JSON.stringify(column)}`)
|
|
else assert(['auto', 'scroll'].includes(column.scrolls), `Column must scroll on its own: ${JSON.stringify(column)}`)
|
|
}
|
|
if (width === 1440) {
|
|
const heading = await js(`(() => {
|
|
const title = document.querySelector('.supervisor-workspace__canvas-title'), [name, meta] = title.children;
|
|
return { name: name.textContent, meta: meta.textContent, metaBelow: meta.getBoundingClientRect().top >= name.getBoundingClientRect().bottom - 1,
|
|
actionBarAbove: !!document.querySelector('[role=tabpanel] > .supervisor-workspace__action-bar'), height: document.querySelector('.supervisor-workspace__canvas-heading').getBoundingClientRect().height };
|
|
})()`)
|
|
assert(heading.metaBelow && heading.meta.includes('图谱范围') && !heading.actionBarAbove && heading.height <= 80, `Scope line belongs under the canvas title: ${JSON.stringify(heading)}`)
|
|
reports.push({ heading })
|
|
}
|
|
assert(Math.abs((fill.graphArea.playbackBottom ?? fill.graphArea.map.bottom) - fill.graphArea.canvasBottom) <= 1,
|
|
`Graph area must reach the column bottom: ${JSON.stringify(fill.graphArea)}`)
|
|
await writeFile(join(artifacts, `fill-${width}x${height}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
win.setContentSize(1440, 1100)
|
|
await js(
|
|
'document.querySelectorAll(".supervisor-workspace__graph-list [role=tab]")[2].click()'
|
|
)
|
|
await settle()
|
|
await js('document.querySelector(".supervisor-workspace__list-panel button:last-of-type").click()')
|
|
await settle()
|
|
assert(
|
|
await js('document.querySelectorAll("[data-relation]").length === 1'),
|
|
'Selected relation endpoints must remain visible'
|
|
)
|
|
await js('document.querySelectorAll(".supervisor-workspace__graph-list [role=tab]")[0].click()')
|
|
await settle()
|
|
const target = await js(
|
|
`(() => { const e = document.querySelectorAll('.supervisor-workspace__list-panel button')[79]; e.scrollIntoView({block:'center'}); const r = e.getBoundingClientRect(); return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), hit: e.contains(document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2)) }; })()`
|
|
)
|
|
assert(target.hit, 'Last event is not reachable')
|
|
win.webContents.sendInputEvent({
|
|
type: 'mouseDown',
|
|
x: target.x,
|
|
y: target.y,
|
|
button: 'left',
|
|
clickCount: 1
|
|
})
|
|
win.webContents.sendInputEvent({
|
|
type: 'mouseUp',
|
|
x: target.x,
|
|
y: target.y,
|
|
button: 'left',
|
|
clickCount: 1
|
|
})
|
|
await settle()
|
|
assert(
|
|
await js(
|
|
`!!document.querySelector('.supervisor-workspace__map [aria-label="模拟事件 80"][aria-pressed=true]')`
|
|
)
|
|
)
|
|
assert.equal(
|
|
await js(
|
|
'document.querySelectorAll(".supervisor-workspace__list-panel button").length'
|
|
),
|
|
80
|
|
)
|
|
const positions = await js(
|
|
'[...document.querySelectorAll(".supervisor-workspace__node:not(.supervisor-workspace__entity) circle")].map(e => [e.getAttribute("cx"), e.getAttribute("cy")].join())'
|
|
)
|
|
assert.equal(new Set(positions).size, 8)
|
|
assert(await js(`(() => {
|
|
return [...document.querySelectorAll('.supervisor-workspace__event-index')].every(e => {
|
|
const range = document.createRange(); range.selectNodeContents(e);
|
|
const tops = [...range.getClientRects()].map(r => r.top);
|
|
return Math.max(...tops) - Math.min(...tops) < 1;
|
|
});
|
|
})()`), 'Dense event numbers must not wrap')
|
|
await js(
|
|
'document.querySelector(".supervisor-workspace__graph-canvas").scrollIntoView({block:"start"})'
|
|
)
|
|
await settle()
|
|
await writeFile(
|
|
join(artifacts, 'dense-selected.png'),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
await js('document.querySelector("#supervisor-tab-settings").click()')
|
|
await settle()
|
|
assert.equal(
|
|
await js('document.querySelectorAll("h1").length'),
|
|
1,
|
|
'Duplicate settings h1'
|
|
)
|
|
await writeFile(
|
|
join(artifacts, 'settings.png'),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
assert.equal(await js('document.querySelectorAll("[role=tablist]").length'), 1, 'No nested settings menu')
|
|
for (const width of [1440, 1024, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
await js('document.querySelector(".page-shell").scrollTop = 0')
|
|
await settle()
|
|
const sizes = await js(`(() => {
|
|
const shell = document.querySelector('.page-shell');
|
|
return { viewport: innerWidth, page: document.documentElement.scrollWidth,
|
|
clientWidth: shell.clientWidth, scrollWidth: shell.scrollWidth, headings: document.querySelectorAll('h1').length };
|
|
})()`)
|
|
assert(
|
|
sizes.page <= width && sizes.scrollWidth <= sizes.clientWidth,
|
|
'Settings overflow'
|
|
)
|
|
assert.equal(sizes.headings, 1)
|
|
reports.push({ settings: sizes })
|
|
await writeFile(
|
|
join(artifacts, `settings-plans-${width}.png`),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
}
|
|
for (const query of [
|
|
'story=1',
|
|
'story=2',
|
|
'long=1',
|
|
'state=loading',
|
|
'state=error',
|
|
'state=unavailable',
|
|
'state=empty'
|
|
]) {
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?' + query)
|
|
await wait('!!document.querySelector("#supervisor-tab-graph")')
|
|
await js('document.querySelector("#supervisor-tab-graph").click()')
|
|
await wait(
|
|
query === 'state=loading'
|
|
? '!!document.querySelector(".empty-state")'
|
|
: '!document.querySelector(".supervisor-workspace[aria-busy=true]")'
|
|
)
|
|
for (const width of [1440, 1024, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
await settle()
|
|
assert(
|
|
await js('document.documentElement.scrollWidth <= innerWidth'),
|
|
`Overflow: ${query}/${width}`
|
|
)
|
|
if (!query.startsWith('state=')) {
|
|
const labels = await js(
|
|
'[...document.querySelectorAll(".supervisor-workspace__map text")].map(e => { const b = e.getBBox(); return { text:e.textContent, x:b.x, y:b.y, width:b.width, height:b.height } })'
|
|
)
|
|
for (const [index, a] of labels.entries()) {
|
|
for (const b of labels.slice(index + 1)) {
|
|
assert(
|
|
!(
|
|
a.x < b.x + b.width &&
|
|
a.x + a.width > b.x &&
|
|
a.y < b.y + b.height &&
|
|
a.y + a.height > b.y
|
|
),
|
|
`Overlapping ${query}: ${a.text} / ${b.text}`
|
|
)
|
|
}
|
|
}
|
|
reports.push({ scenario: query, viewport: width, labels })
|
|
}
|
|
await writeFile(
|
|
join(artifacts, `${query.replace('=', '-')}-${width}.png`),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
}
|
|
}
|
|
await js('document.querySelector("#supervisor-tab-overview").click()')
|
|
await settle()
|
|
await js(
|
|
'document.querySelector(".supervisor-workspace__toolbar .primary-button").click()'
|
|
)
|
|
await wait(
|
|
'!!document.querySelector(".supervisor-workspace__inline-error")'
|
|
)
|
|
assert(
|
|
await js(
|
|
'document.querySelector("[role=alert]").textContent.includes("模型暂时不可用")'
|
|
)
|
|
)
|
|
await writeFile(
|
|
join(artifacts, 'manual-review-error-390.png'),
|
|
(await win.webContents.capturePage()).toPNG()
|
|
)
|
|
await js('document.querySelector("#supervisor-tab-settings").click()')
|
|
await wait('!!document.querySelector(".heartbeat-settings")')
|
|
assert(await js('!document.querySelector("#supervisor-panel-settings [role=tablist], #supervisor-panel-settings .scope-badge")'))
|
|
assert(await js('!document.querySelector("#heartbeat-panel-plans")'), 'Plans must not live in settings')
|
|
await js('document.querySelector("#supervisor-tab-plans").click()')
|
|
await js('document.querySelector(".heartbeat-settings__intro button").focus(); document.querySelector(".heartbeat-settings__intro button").click()')
|
|
await wait('!!document.querySelector("[role=dialog]")')
|
|
assert(await js('document.activeElement === document.querySelector("[role=dialog] input")'), 'Plan initial focus')
|
|
await js(`(() => {
|
|
const select = document.querySelector('[aria-label="监督频率"]');
|
|
select.value = 'weekly'; select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
})()`)
|
|
await wait('!!document.querySelector("[aria-label=监督星期]")')
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const width of [1440, 1024, 390]) {
|
|
win.setContentSize(width, 1100)
|
|
await settle()
|
|
const settings = await js(`(() => {
|
|
const root = document.querySelector('[role=dialog]');
|
|
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
|
|
clientWidth: root.clientWidth, scrollWidth: root.scrollWidth,
|
|
fields: [...root.querySelectorAll('input, select')].filter(e => e.getClientRects().length).map(e => {
|
|
const r = e.getBoundingClientRect();
|
|
return { left: r.left, right: r.right, height: r.height };
|
|
}), text: root.textContent };
|
|
})()`)
|
|
assert(settings.pageWidth <= width && settings.scrollWidth <= settings.clientWidth, 'Settings overflow')
|
|
assert(settings.fields.every(r => r.left >= 0 && r.right <= width && r.height >= 28), 'Clipped settings fields')
|
|
reports.push({ scenario: 'automatic-supervision-modal', theme, ...settings })
|
|
await writeFile(join(artifacts, `plan-modal-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
if (width === 390) {
|
|
win.setContentSize(390, 720)
|
|
await js('document.querySelector("[role=dialog] .heartbeat-settings__submit").scrollIntoView({block:"end"})')
|
|
await settle()
|
|
assert(await js(`(() => {
|
|
const button = document.querySelector('[role=dialog] .heartbeat-settings__submit');
|
|
const r = button.getBoundingClientRect();
|
|
return r.left >= 16 && r.right <= innerWidth - 16 && r.bottom <= innerHeight && button.contains(document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2));
|
|
})()`), 'Short modal save button must be reachable')
|
|
await writeFile(join(artifacts, `plan-modal-${theme}-390-short.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
}
|
|
}
|
|
await js('document.querySelector("[role=dialog]").dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))')
|
|
await wait('document.querySelector("[role=dialog]").textContent.includes("放弃未保存")')
|
|
await js('document.querySelector("[role=dialog] .danger-solid").click()')
|
|
await wait('!document.querySelector("[role=dialog]")')
|
|
assert(await js('document.activeElement === document.querySelector(".heartbeat-settings__intro button")'), 'Plan focus restored')
|
|
await js('document.querySelector("#supervisor-tab-activity").click()')
|
|
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const width of [1440, 1024, 390]) {
|
|
win.setContentSize(width, width === 390 ? 720 : 1100)
|
|
await settle()
|
|
const activity = await js(`(() => {
|
|
const root = document.querySelector('.supervisor-activity');
|
|
const tabs = document.querySelector('#supervisor-tab-activity').closest('[role=tablist]');
|
|
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
|
|
clientWidth: root.clientWidth, scrollWidth: root.scrollWidth,
|
|
tabsHeight: tabs.getBoundingClientRect().height,
|
|
fields: [...root.querySelectorAll('dd, button')].map(e => {
|
|
const r = e.getBoundingClientRect(); return { left: r.left, right: r.right };
|
|
}), text: root.textContent };
|
|
})()`)
|
|
assert(activity.pageWidth <= width && activity.scrollWidth <= activity.clientWidth, 'Activity overflow')
|
|
assert(activity.tabsHeight >= 32, 'Activity tabs must not collapse')
|
|
assert(activity.fields.every(r => r.left >= 0 && r.right <= width), 'Clipped activity metadata or actions')
|
|
assert(activity.text.includes('运行中') && activity.text.includes('失败') && activity.text.includes('已完成'), 'Missing execution states')
|
|
reports.push({ scenario: 'supervisor-activity', theme, ...activity })
|
|
await writeFile(join(artifacts, `activity-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
}
|
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL + '?menu=1')
|
|
await wait('!!document.querySelector(".supervisor-workspace__recap")')
|
|
win.show()
|
|
win.focus()
|
|
for (const theme of ['light', 'dark']) {
|
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
|
for (const width of [1440, 390]) {
|
|
win.setContentSize(width, width === 390 ? 720 : 1100)
|
|
for (const tab of ['overview', 'graph', 'plans', 'settings', 'activity']) {
|
|
await js(`document.querySelector('#supervisor-tab-${tab}').click(); document.querySelector('.page-shell').scrollTop = 0`)
|
|
await settle()
|
|
const menuLayout = await js(`(() => {
|
|
const panel = document.querySelector('#supervisor-panel-${tab}');
|
|
return { width: innerWidth, pageWidth: document.documentElement.scrollWidth,
|
|
panelWidth: panel.clientWidth, scrollWidth: panel.scrollWidth,
|
|
tabs: [...document.querySelectorAll('[role=tab][id^="supervisor-tab-"]')].map(e => e.textContent),
|
|
nestedMenus: panel.querySelectorAll('[role=tablist]').length,
|
|
reports: !!panel.querySelector('#heartbeat-reports-title'),
|
|
reportWidth: panel.querySelector('#heartbeat-panel-history > section')?.getBoundingClientRect().width,
|
|
suggestions: !!panel.querySelector('#supervision-suggestions-title'),
|
|
suggestionItems: panel.querySelectorAll('.supervision-suggestions__list > li').length,
|
|
audit: !!panel.querySelector('#heartbeat-runs-title'),
|
|
plans: !!panel.querySelector('#heartbeat-panel-plans'),
|
|
settingsBadge: !!panel.querySelector('.scope-badge'),
|
|
headings: [...panel.querySelectorAll('h2')].map(e => e.textContent),
|
|
refresh: [...panel.querySelectorAll('button')].some(e => e.textContent.includes('刷新')) };
|
|
})()`)
|
|
assert.deepEqual(menuLayout.tabs, ['工作回顾', '故事线图谱', '智能心跳', '活动记录', '设置'])
|
|
assert.equal(menuLayout.nestedMenus, tab === 'graph' ? 1 : 0)
|
|
assert(menuLayout.pageWidth <= width && menuLayout.scrollWidth <= menuLayout.panelWidth, 'Menu panel overflow')
|
|
assert.equal(menuLayout.reports, tab === 'plans')
|
|
if (tab === 'plans') assert(Math.abs(menuLayout.reportWidth - menuLayout.panelWidth) <= 1, 'Reports must use full reading width')
|
|
assert.equal(menuLayout.suggestions, tab === 'plans')
|
|
assert.equal(menuLayout.suggestionItems, tab === 'plans' ? 2 : 0)
|
|
assert.equal(menuLayout.audit, false, 'Run audit belongs to Activity, not Smart heartbeat')
|
|
assert.equal(menuLayout.plans, tab === 'plans')
|
|
if (tab === 'settings') {
|
|
assert(!menuLayout.settingsBadge && !menuLayout.refresh, 'Redundant settings badge/refresh')
|
|
assert(menuLayout.headings.includes('回顾算法') && menuLayout.headings.includes('模型超时与并发'))
|
|
}
|
|
reports.push({ scenario: 'menu-placement', theme, tab, ...menuLayout })
|
|
await writeFile(join(artifacts, `menu-${tab}-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
|
const anchors = tab === 'settings' ? ['.heartbeat-settings__editor:last-child']
|
|
: tab === 'plans' ? ['#heartbeat-panel-overview', '.supervision-suggestions', '#heartbeat-panel-history'] : []
|
|
for (const [index, anchor] of anchors.entries()) {
|
|
await js(`document.querySelector('${anchor}').scrollIntoView({block:'start'})`)
|
|
await settle()
|
|
await writeFile(join(artifacts, `menu-${tab}-${theme}-${width}-section-${index}.png`), (await win.webContents.capturePage()).toPNG())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
await js('document.querySelector("#supervisor-tab-plans").click()')
|
|
await js('[...document.querySelectorAll(".heartbeat-settings__actions button")].find(b => b.textContent === "执行记录").click()')
|
|
await wait('document.querySelector(".supervisor-activity select")?.value === "plan" && document.querySelectorAll(".supervisor-activity__item").length === 2')
|
|
await js('document.querySelector(".page-shell").scrollTop = 0')
|
|
await settle()
|
|
await writeFile(join(artifacts, 'activity-exact-plan.png'), (await win.webContents.capturePage()).toPNG())
|
|
await js('document.querySelector("#supervisor-tab-settings").focus()')
|
|
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Left' })
|
|
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Left' })
|
|
await settle()
|
|
assert(await js('document.activeElement.id === "supervisor-tab-activity" && document.activeElement.getAttribute("aria-selected") === "true"'), 'Native keyboard tab navigation')
|
|
assert.deepEqual(errors, [])
|
|
await writeFile(
|
|
join(artifacts, 'measurements.json'),
|
|
JSON.stringify(reports, null, 2)
|
|
)
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
artifacts,
|
|
cases: reports.map(({ texts, tokens, ...report }) => ({
|
|
...report,
|
|
svgTexts: texts?.length,
|
|
resolvedTokens: tokens && Object.keys(tokens).length
|
|
})),
|
|
errors
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
win.destroy()
|
|
app.quit()
|
|
})
|
|
.catch((error) => {
|
|
console.error(error)
|
|
app.exit(1)
|
|
})
|