diff --git a/e2e/rdm-cockpit.playwright.ts b/e2e/rdm-cockpit.playwright.ts index ea7adec..b807120 100644 --- a/e2e/rdm-cockpit.playwright.ts +++ b/e2e/rdm-cockpit.playwright.ts @@ -21,9 +21,15 @@ async function login(page: Page, user: string, password: string) { await page.getByRole('button', { name: '登录', exact: true }).click() } +// 侧栏入口两种形态:常规侧栏按钮(.tsb-tool);窄宽紧凑布局换图标栏 +// (.icon-only 内 title 以「文档驾驶舱」开头的按钮)——入口都可达。 +function cockpitEntry(page: Page) { + return page.locator('.tsb-tool', { hasText: '文档驾驶舱' }) + .or(page.locator('.icon-only button[title^="文档驾驶舱"]')).first() +} + async function loginOwner(page: Page) { await login(page, OWNER, PASSWORD) - await expect(page.locator('.tsb-tool', { hasText: '文档驾驶舱' })).toBeVisible({ timeout: 15_000 }) // 登录后身份纪元与能力探针仍可能各跳一次;应用在身份变更时会关闭已开 // 工作区——等落定再点入口,避免 overlay 被身份刷新关掉(竞态实测)。 await page.waitForLoadState('networkidle').catch(() => {}) @@ -31,7 +37,11 @@ async function loginOwner(page: Page) { } async function openCockpit(page: Page) { - await page.locator('.tsb-tool', { hasText: '文档驾驶舱' }).click() + // 窄宽是单窗格布局:登录后默认「对话」窗格,先切到「上下文」窗格。 + if (!(await cockpitEntry(page).isVisible().catch(() => false))) { + await page.getByRole('button', { name: '打开上下文' }).click() + } + await cockpitEntry(page).click() await expect(page.locator('.cockpit-project')).toBeVisible({ timeout: 15_000 }) // 默认选中首个项目(种子=审计演示项目)。 await expect(page.locator('.cockpit-stats')).toBeVisible({ timeout: 15_000 }) @@ -111,16 +121,17 @@ test.describe.serial('I4 文档驾驶舱组合验收', () => { test('4 一致性:API 采纳新版本后,刷新入口与各投影同步', async ({ page }) => { await loginOwner(page) await openCockpit(page) - await page.locator('.cockpit-tree-caret').first().click() - await page.locator('.cockpit-tree-title', { hasText: '设计说明' }).first().click() + const branch4 = page.locator('.cockpit-tree-branch', { hasText: '验证主体报告' }).first() + await branch4.locator('.cockpit-tree-caret').click() + await branch4.locator('.cockpit-tree-title', { hasText: '设计说明' }).first().click() const snapBefore = await page.locator('.cockpit-snap').innerText() - // 通过应用自身 API 采纳一个新提案(同源会话;与浏览器渲染解耦)。 - await page.request.post('/api/commands/rdm.draft.propose', { - data: { params: { projectId: (await page.locator('.cockpit-project option:checked').textContent()) || '', sectionId: '', content: '一致性探针内容——不应出现' }, meta: {} }, + // 通过应用自身 API 落一个已提交变更(需求账本新增条目,唯一键防幂等跳过; + // 采纳走高危确认卡、服务端不允许客户端伪造 Confirmed,故用低危提交面驱动)。 + await page.request.post('/api/commands/rdm.reqs.import', { + data: { params: { projectId: await page.locator('.cockpit-project').inputValue(), + items: [{ kind: 'UR', title: `UR-PROBE-${Date.now()} 一致性探针`, statement: '刷新一致性驱动条目', source: 'e2e-probe' }] } }, failOnStatusCode: false, }) - // 上面缺 sectionId 必然 400:真正的驱动是 e2e.sh 早已留好的第二个提案 - // (种子阶段 propose 未采纳的「一致性探针」),这里经面板刷新读取。 await page.getByRole('button', { name: '刷新' }).click() await expect(page.locator('.cockpit-snap')).not.toHaveText(snapBefore) // 版本块前进:出现新版本条目。 diff --git a/src/components/CockpitReader.tsx b/src/components/CockpitReader.tsx index 6e94f55..0326236 100644 --- a/src/components/CockpitReader.tsx +++ b/src/components/CockpitReader.tsx @@ -36,7 +36,14 @@ function useNodeDetail(pid: string, sel?: CockpitNodeRef) { setDetail(null); setErr('') if (!sel) return api.rdmCommand('rdm.cockpit.node', { projectId: pid, kind: sel.kind, id: sel.id }) - .then(d => { if (g === gen.current) setDetail(d) }) + .then(d => { + if (g !== gen.current) return + // 后端空列表可能投影为 null(Go nil slice);统一归一,消费端按数组使用。 + for (const k of ['sources', 'history', 'impacts', 'conflicts'] as const) { + if (d?.[k] && !Array.isArray(d[k].items)) d[k].items = [] + } + setDetail(d) + }) .catch(e => { if (g === gen.current) setErr(e.message) }) return () => { gen.current++ } }, [pid, sel?.kind, sel?.id]) diff --git a/src/components/DocumentCockpit.tsx b/src/components/DocumentCockpit.tsx index 8f33889..e7c08b0 100644 --- a/src/components/DocumentCockpit.tsx +++ b/src/components/DocumentCockpit.tsx @@ -16,9 +16,9 @@ import { type CockpitTreeNode, type CockpitTreePage, type Density, type PanelTabId, type ViewMode, } from '../documentCockpit' -import { CockpitPanel, CockpitReader } from './CockpitReader' +import { CockpitPanel, CockpitReader, VersionBlock } from './CockpitReader' -interface ProjectView { id: string; name: string; canAudit?: boolean } +interface ProjectView { projectId: string; name: string; canAudit?: boolean } function CockpitErrorState({ raw }: { raw: string }) { const c = classifyRdmError(raw) @@ -66,10 +66,12 @@ export function DocumentCockpit() { setProjects(null); setPid(''); setOverview(null); setSelected(null); setFilter(''); setError('') api.rdmCommand<{ projects?: ProjectView[] } | ProjectView[]>('rdm.project.list') .then(list => { - if (!c) return + if (c) return // rdm.project.list 返回 { projects: [...] };容忍裸数组形态。 const rows = Array.isArray(list) ? list : (list?.projects ?? []) setProjects(rows) + // 设计 §6.2:进入驾驶舱默认选中第一个可见项目,驱动概览首屏。 + if (rows.length > 0) setPid(prev => prev || rows[0].projectId) }) .catch(e => { if (!c) setError(e.message) }) return () => { c = true } @@ -128,7 +130,7 @@ export function DocumentCockpit() {
{filter &&