Why: buildWorkspaceEntries pushed only BUILTINS[0] (agents); the U4 rdm-audit workspace was declared but never ranked, so no user could open the trace/audit workspace from the sidebar. Evidence: vitest workspaces.test.ts 4/4 (updated expectations plus a new overflow-truncation case); full browser run in scripts/e2e.sh. Impact: sidebar gains the second builtin tool; server-declared workspaces keep their shortcut precedence. Refs: U4 (Dev #368) regression; task feature/rdm-e2e-acceptance.
116 lines
6.6 KiB
TypeScript
116 lines
6.6 KiB
TypeScript
// U4 运行验收:真实浏览器走通项目追溯审计工作区(RdmAuditWorkspace 四入口)。
|
||
// 驱动方:Dev 仓 scripts/e2e.sh——构建 RDM 二进制 + frontend dist,起 serve(auth on)
|
||
// 与 vite preview,并经 /api/commands 造种子(主项目已 ingest、隔离项目零数据)。
|
||
// 环境契约(e2e.sh 注入;缺省值与 e2e.sh 保持一致):
|
||
// PLAYWRIGHT_BASE_URL 前端入口(默认 http://127.0.0.1:4173)
|
||
// E2E_OWNER/E2E_PASSWORD 登录账号(域管理员+基座管理员)
|
||
// E2E_PROJECT_MAIN 已 ingest 的主项目名
|
||
// E2E_PROJECT_ISOLATED 零数据隔离项目名
|
||
import { test, expect, type Page } from '@playwright/test'
|
||
|
||
const OWNER = process.env.E2E_OWNER || 'e2e-auditor'
|
||
const PASSWORD = process.env.E2E_PASSWORD || 'E2eAudit#2026'
|
||
const PASSWORD2 = process.env.E2E_PASSWORD2 || 'E2eAudit#2026b'
|
||
const MAIN = process.env.E2E_PROJECT_MAIN || '审计演示项目'
|
||
const ISOLATED = process.env.E2E_PROJECT_ISOLATED || '隔离检查项目'
|
||
|
||
test.skip(!process.env.PLAYWRIGHT_BASE_URL, 'set PLAYWRIGHT_BASE_URL (scripts/e2e.sh drives this suite)')
|
||
|
||
async function login(page: Page) {
|
||
await page.goto('/')
|
||
await expect(page.getByPlaceholder('由管理员分配')).toBeVisible()
|
||
await page.getByPlaceholder('由管理员分配').fill(OWNER)
|
||
// 首个用例用初始密码(触发强制改密门),后续用例用改后的新密码。
|
||
await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD)
|
||
await page.getByRole('button', { name: '登录', exact: true }).click()
|
||
const gate = page.getByText('修改密码')
|
||
const gated = await gate.waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false)
|
||
if (gated) {
|
||
const pwInputs = page.locator('.lg-card input[type="password"]')
|
||
await pwInputs.nth(0).fill(PASSWORD)
|
||
await pwInputs.nth(1).fill(PASSWORD2)
|
||
await pwInputs.nth(2).fill(PASSWORD2)
|
||
await page.getByRole('button', { name: '设置新密码并进入' }).click()
|
||
} else {
|
||
// 已改密:改用新密码重登。
|
||
await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD2)
|
||
await page.getByRole('button', { name: '登录', exact: true }).click()
|
||
}
|
||
// 登录成功后侧栏体系工具出现「追溯审计」入口。
|
||
await expect(page.locator('.tsb-tool', { hasText: '追溯审计' })).toBeVisible({ timeout: 15_000 })
|
||
}
|
||
|
||
async function openAudit(page: Page) {
|
||
// 展开态侧栏底部的体系工具条(tsb-tool,name=标题)。
|
||
await page.locator('.tsb-tool', { hasText: '追溯审计' }).click()
|
||
await expect(page.getByText('追溯审计').first()).toBeVisible()
|
||
await expect(page.locator('.rdm-audit-project')).toBeVisible()
|
||
}
|
||
|
||
test.describe.serial('U4 项目追溯审计工作区', () => {
|
||
test('登录并打开工作区,概览计数与覆盖标注可读', async ({ page }) => {
|
||
await login(page)
|
||
await openAudit(page)
|
||
// 概览:统计卡片 + 水位标注(数据完整性提示,不冒充实时)。
|
||
await expect(page.locator('.rdm-audit-card')).toHaveCount(9)
|
||
await expect(page.getByText(/截至水位 \d+/)).toBeVisible()
|
||
// 主项目应有切片与章节(种子 ingest 保证)。
|
||
await expect(page.locator('.rdm-audit-card', { hasText: '切片' })).toContainText(/\d+/)
|
||
await expect(page.locator('.rdm-audit-card', { hasText: '章节' })).not.toContainText(/^0$/)
|
||
})
|
||
|
||
test('三次主要点击内完成取证:章节来源依据与最近修改原因', async ({ page }) => {
|
||
await login(page)
|
||
await openAudit(page)
|
||
// 路径①:资料与文档(点击1)→ 选章节(点击2)→ 对象详情含最近事件(修改原因)。
|
||
await page.getByRole('tab', { name: '资料与文档' }).click()
|
||
await expect(page.locator('.rdm-audit-tree-node')).not.toHaveCount(0)
|
||
await page.locator('.rdm-audit-tree-node').first().click()
|
||
await expect(page.locator('.rdm-audit-detail')).toBeVisible()
|
||
await expect(page.getByText('最近事件')).toBeVisible()
|
||
// 路径②:关联追溯(点击1)→ 选章节(点击2)→ 来源依据/影响对象分列。
|
||
await page.getByRole('tab', { name: '关联追溯' }).click()
|
||
await expect(page.locator('.rdm-audit-pick button')).not.toHaveCount(0)
|
||
await page.locator('.rdm-audit-pick button').first().click()
|
||
await expect(page.getByText(/来源依据(\d+)/)).toBeVisible()
|
||
await expect(page.getByText(/影响对象(\d+)/)).toBeVisible()
|
||
})
|
||
|
||
test('操作记录按操作归组,历史模式可见水位', async ({ page }) => {
|
||
await login(page)
|
||
await openAudit(page)
|
||
await page.getByRole('tab', { name: '操作记录' }).click()
|
||
// 事件以单行或操作组呈现;种子项目必有 ingest 事件。
|
||
await expect(
|
||
page.locator('.rdm-audit-event').or(page.locator('.rdm-audit-opgroup')),
|
||
).not.toHaveCount(0)
|
||
// 历史模式:返回当前切换明确,无对象时输入提示可理解。
|
||
await page.getByRole('button', { name: '历史模式' }).click()
|
||
await expect(page.getByText(/截至 seq \d+/)).toBeVisible()
|
||
await page.getByRole('button', { name: '← 返回当前' }).click()
|
||
await expect(page.getByRole('button', { name: '历史模式' })).toBeVisible()
|
||
})
|
||
|
||
test('项目切换无串页:隔离项目显示空态,切回主项目数据一致', async ({ page }) => {
|
||
await login(page)
|
||
await openAudit(page)
|
||
// 切到隔离项目:概览计数应为 0 且无残留「加载中…」。
|
||
await page.locator('.rdm-audit-project').selectOption({ label: ISOLATED })
|
||
await expect(page.getByText('加载中…')).toHaveCount(0, { timeout: 10_000 })
|
||
await expect(page.locator('.rdm-audit-card', { hasText: '切片' })).toContainText(/^0$/)
|
||
// 空态:文档树与时间线给出可理解提示。
|
||
await page.getByRole('tab', { name: '资料与文档' }).click()
|
||
await expect(page.getByText('暂无文档')).toBeVisible()
|
||
await page.getByRole('tab', { name: '操作记录' }).click()
|
||
await expect(page.getByText('暂无事件')).toBeVisible()
|
||
// 立即切回主项目:标题与请求一致,数据回到非零(过期响应被丢弃,无串页)。
|
||
await page.getByRole('tab', { name: '概览' }).click()
|
||
await page.locator('.rdm-audit-project').selectOption({ label: MAIN })
|
||
await expect(page.getByText('加载中…')).toHaveCount(0, { timeout: 10_000 })
|
||
await expect(page.locator('.rdm-audit-card', { hasText: '切片' })).toContainText(/[1-9]/)
|
||
await expect(page.locator('.rdm-audit-project')).toHaveValue(
|
||
await page.locator('.rdm-audit-project option', { hasText: MAIN }).getAttribute('value') ?? '',
|
||
)
|
||
})
|
||
})
|