Files
Agent-System-Frontend/e2e/rdm-audit.playwright.ts
T
weichenandCursor d9bb780384 Add an audit overview export that stays outside the readiness gate
Why: auditors need to generate and download a watermarked report from the overview, and that must not appear when the export commands are off or change whether the workspace itself opens.
Evidence: SELF npm test, 138 passed. Playwright download case is in e2e/rdm-audit.playwright.ts and runs under scripts/e2e.sh.
Impact: the overview shows type, generate, draft confirmation, watermark, sha256, expiry, and download only when the three export commands are enabled.
Refs: RDM pull request 301
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 15:38:00 +00:00

147 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 项目追溯审计工作区', () => {
// 卡片按 label 精确定位(「章节」与「空缺章节」等子串互不干扰)。
const card = (page: Page, label: string) =>
page.locator('.rdm-audit-card').filter({ has: page.locator('.rdm-audit-card-label', { hasText: new RegExp(`^${label}$`) }) })
test('登录并打开工作区,概览计数与覆盖标注可读', async ({ page }) => {
await login(page)
await openAudit(page)
// 概览:统计卡片 + 水位标注(数据完整性提示,不冒充实时)。
await expect(page.locator('.rdm-audit-card')).toHaveCount(9)
await expect(page.getByText(/截至水位 \d+/)).toBeVisible()
// 主项目应有切片与章节(种子 structure 装载 + ingest 保证)。
await expect(card(page, '切片').locator('.rdm-audit-card-num')).toHaveText(/[1-9]/)
await expect(card(page, '章节')).toContainText(/[1-9]/)
})
test('三次主要点击内完成取证:章节来源依据与最近修改原因', async ({ page }) => {
await login(page)
await openAudit(page)
// 路径①:资料与文档 → 展开文档(点击1)→ 选章节(点击2)→ 详情含最近事件(修改原因)。
// docs 树是层级结构:第一层是文档,章节在展开后的第二层。
await page.getByRole('tab', { name: '资料与文档' }).click()
await page.locator('.rdm-audit-tree-node').first().click()
const sectionNode = page.locator('.rdm-audit-tree-children .rdm-audit-tree-node')
.filter({ has: page.locator('.rdm-audit-tree-kind', { hasText: 'section' }) }).first()
await expect(sectionNode).toBeVisible()
await sectionNode.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('← 返回')).toBeVisible()
await expect(
page.getByText(/来源依据(\d+)/).or(page.getByText('没有已知关联')),
).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(card(page, '切片').locator('.rdm-audit-card-num')).toHaveText('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(card(page, '切片').locator('.rdm-audit-card-num')).toHaveText(/[1-9]/)
await expect(page.locator('.rdm-audit-project')).toHaveValue(
await page.locator('.rdm-audit-project option', { hasText: MAIN }).getAttribute('value') ?? '',
)
})
test('概览导出:生成后显示水位与校验和,并可下载', async ({ page }) => {
await login(page)
await openAudit(page)
const exportBox = page.locator('.rdm-audit-export')
await expect(exportBox).toBeVisible()
await exportBox.getByLabel('导出类型').selectOption('cgraph-docx')
await exportBox.getByRole('button', { name: '生成', exact: true }).click()
const draft = exportBox.getByRole('button', { name: '生成草稿' })
if (await draft.waitFor({ state: 'visible', timeout: 8_000 }).then(() => true).catch(() => false)) {
await draft.click()
}
await expect(exportBox.getByText(/水位 \d+/)).toBeVisible({ timeout: 15_000 })
await expect(exportBox.getByText(/sha256 [0-9a-f]{64}/)).toBeVisible()
await expect(exportBox.getByText(/有效期/)).toBeVisible()
const download = page.waitForEvent('download')
await exportBox.getByRole('button', { name: '下载' }).click()
const file = await download
expect(file.suggestedFilename()).toMatch(/\.docx$/)
})
})