From de4f818c80b11dc56128087344d18b97f2e3d7ca Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 22 Jul 2026 13:15:51 +0800 Subject: [PATCH] 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. --- .../client/connection/src/client/fixture.ts | 12 ++ scripts/verify-todo-display.mjs | 107 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 scripts/verify-todo-display.mjs diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index e1e21dfd78..82b3925aac 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -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[] } diff --git a/scripts/verify-todo-display.mjs b/scripts/verify-todo-display.mjs new file mode 100644 index 0000000000..3e8f42e5fe --- /dev/null +++ b/scripts/verify-todo-display.mjs @@ -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()