test(gui): todo display fixture sample + browser acceptance script

fx-alpha gains turn 63: a todo_write call/result pair plus the todo/write
snapshot event, feeding both the TodoRow toolview and the TodoPanel strip
in ?fixture mode. verify-todo-display.mjs drives chromium through panel
visibility, content, row summary, details linkage, collapse and dark.
This commit is contained in:
Chinesezjc
2026-07-22 13:15:51 +08:00
parent 63109dab66
commit de4f818c80
2 changed files with 119 additions and 0 deletions

View File

@@ -124,6 +124,18 @@ function buildAlphaLog(): SessionEvent[] {
toolTurn(61, 'fx-write', '{"path":"notes/demo.txt","content":"hello fixture\\n"}', 'wrote notes/demo.txt')
toolTurn(62, 'edit', '{"file_path":"notes/demo.txt","old_string":"hello","new_string":"hello fixture"}', '已编辑')
toolTurn(63, 'write', '{"file_path":"notes/new-demo.txt","content":"hello fixture\\n"}', '已写入')
// Turn 64: todo_write sample — the TodoRow toolview in the flow plus the
// todo/write snapshot event feeding the TodoPanel plan strip.
const fixtureTodos = [
{ content: '梳理需求', status: 'completed' },
{ content: '实现 fixture 样本', status: 'in_progress' },
{ content: '浏览器验收', status: 'pending' },
]
const todoArgs = JSON.stringify({ todos: fixtureTodos })
toolTurn(64, 'todo_write', todoArgs, 'Updated todo list: 1 pending, 1 in progress, 1 completed.')
// The tool appends the snapshot event inside its own turn; splice it before the trailing turn/end.
events.splice(events.length - 1, 0, { type: 'todo/write', time: time += 800, data: { todos: fixtureTodos } })
events.forEach((e, i) => { e.seq = i })
return events as unknown as SessionEvent[]
}

View File

@@ -0,0 +1,107 @@
// Manual acceptance probe: boot the real shell + 8 bundles in ?fixture mode,
// open fx-alpha, assert the TodoPanel strip and the todo_write row render.
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import { startWebServer } from '@deepseek-ai/dsh-host-webserver'
// playwright is a dependency of apps/web (the browser test owner), not the root.
const { chromium } = createRequire(new URL('../apps/web/package.json', import.meta.url)).call(undefined, 'playwright')
const root = fileURLToPath(new URL('..', import.meta.url))
const bundle = (dir) => `${root}packages/client/${dir}/lib/client.js`
const PLUGINS = [
{ id: '@deepseek-ai/dsh-client-connection', dir: 'connection', inject: [], immediately: true },
{ id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
{ id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', inject: [], immediately: true },
{ id: '@deepseek-ai/dsh-client-i18n', dir: 'i18n', inject: [], immediately: true },
{ id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', inject: ['@deepseek-ai/dsh-client-runtime'] },
{ id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
{ id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
{ id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
]
for (const p of PLUGINS) if (!existsSync(bundle(p.dir))) throw new Error(`bundle missing: ${p.dir}`)
const rows = PLUGINS.map(p => ({
id: p.id, url: `/plugins/${p.id}/client.js?rev=verify`, rev: 'verify',
...(p.inject.length > 0 ? { inject: p.inject } : {}),
...(p.immediately ? { immediately: true } : {}),
}))
const graph = { rev: 'verify', entries: rows }
const byId = new Map(PLUGINS.map(p => [p.id, bundle(p.dir)]))
const port = 34567
const errors = []
const server = await startWebServer({
host: '127.0.0.1',
port,
distIndex: `${root}apps/web/dist/index.html`,
apiHandler: { fetch: () => Promise.resolve(new Response('fixture mode must not call /api', { status: 500 })) },
webPlugins: { graph: () => graph, clientPath: (id) => byId.get(id), onRebuilt: () => () => undefined },
}, (err) => errors.push(`server: ${String(err)}`))
const browser = await chromium.launch()
const page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
page.on('pageerror', e => errors.push(String(e)))
await page.goto(`http://127.0.0.1:${port}/?fixture`, { waitUntil: 'load' })
try {
await page.waitForSelector('[class*="frame"]', { timeout: 15000 })
} catch (e) {
console.error('BODY:', (await page.evaluate(() => document.body.innerText)).slice(0, 400))
console.error('STATUS:', await page.evaluate(() => JSON.stringify(globalThis
.__DSH_LOADER_STATUS__ ?? 'n/a')))
console.error('BOOT:', await page.evaluate(() => JSON.stringify(window.__DSH_BOOT__)))
const reqs = await page.evaluate(() => performance.getEntriesByType('resource').map(r => `${r.name.split('/').slice(-2).join('/')}=${r.responseStatus ?? '?'}`))
console.error('RES:', reqs.join(' '))
console.error('ERRORS:', errors.join(' ;; '))
throw e
}
// Open fx-alpha: expand the workspace group, then click the newest session row.
await page.locator('[role="treeitem"]').first().click()
// fx-alpha is the newest (running) session — the first option row.
const sessionRow = page.locator('[role="treeitem"][aria-selected]').first()
await sessionRow.waitFor({ timeout: 5000 })
await sessionRow.click()
await page.waitForSelector('[data-testid="todo-panel"]', { timeout: 15000 })
console.log('✓ TodoPanel visible')
const panelText = await page.locator('[data-testid="todo-panel"]').innerText()
for (const expected of ['Plan', '1/3', '梳理需求', '实现 fixture 样本', '浏览器验收']) {
if (!panelText.includes(expected)) throw new Error(`TodoPanel missing "${expected}"; got: ${panelText}`)
}
console.log('✓ TodoPanel content: counts + all three items')
await page.screenshot({ path: `${root}.artifacts/todo-01-panel.png` })
// The todo_write row in the flow (turn 63 sample, already at the bottom).
const row = page.locator('[data-sample="todo-row"]')
await row.waitFor({ timeout: 10000 })
const rowText = await row.innerText()
if (!rowText.includes('更新任务清单') || !rowText.includes('1/3 已完成')) throw new Error(`TodoRow wrong: ${rowText}`)
console.log('✓ TodoRow renders plan summary:', rowText.replace(/\n/g, ' '))
await page.screenshot({ path: `${root}.artifacts/todo-02-row.png` })
// Row click opens details with the raw args.
await row.click()
await page.waitForSelector('text=Input', { timeout: 5000 })
console.log('✓ TodoRow click opens details')
await page.screenshot({ path: `${root}.artifacts/todo-03-details.png` })
// Collapse: list hides, active item hint appears in the header.
await page.locator('[data-testid="todo-panel"] button').first().click()
const collapsed = await page.locator('[data-testid="todo-panel"]').innerText()
if (collapsed.includes('梳理需求')) throw new Error('collapse failed: list still visible')
if (!collapsed.includes('实现 fixture 样本')) throw new Error('collapsed hint missing the active item')
console.log('✓ Collapse hides list, shows active hint')
await page.screenshot({ path: `${root}.artifacts/todo-04-collapsed.png` })
// Dark theme spot check.
await page.evaluate(() => document.body.setAttribute('data-ds-dark-theme', ''))
await page.screenshot({ path: `${root}.artifacts/todo-05-dark.png` })
console.log('✓ Dark screenshot taken')
if (errors.length > 0) throw new Error(`page errors: ${errors.join('; ')}`)
console.log('✓ No page errors — todo display acceptance PASSED')
await browser.close()
await server.close()