Add document render workspace with profiles, preview, review and downloads
Why: unified renderer Task 11 needs a reader/delivery UI over the RDM document tree: four-level rule selection with controlled-document no-fallback explanation, preview with node-linked diagnostics, a structure canvas and a linear reader sharing one selection and filter, review annotations/revisions, and per-delivery downloads. Evidence: npx vitest run -> 35 files, 338 tests passed (45 new in test/document-render-workspace.test.ts, design-guard green); npm run build -> tsc -b + vite build ok; npx knip -> findings identical to the pre-existing baseline (check-knip 43 <= 43); git diff --check clean. e2e/document-renderer.playwright.ts authored, not executed. Impact: new src/documentRenderPresentation.ts (pure helpers) and src/components/DocumentRenderWorkspace.tsx, opened from a per-document "阅读与交付" action in the audit materials tree; store gains an rdmRender capability probe (fail-closed); audit export reuses the shared base64 file saver; token-only styles appended to styles.css. Async state is owned per hook with request guards plus identity epoch, and the workspace remounts per identity/project/document scope. Refs: unified renderer plan Task 11 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// 统一渲染 Task 11 运行验收:真实产品走通「资料与文档 → 阅读与交付」。
|
||||
// 驱动方:Dev 仓 scripts/e2e.sh(同 rdm-audit.playwright.ts 的环境契约);
|
||||
// PLAYWRIGHT_BASE_URL 未设置时整组跳过。种子数据差异(是否有诊断、是否已有审阅提交)
|
||||
// 以「可理解状态」分支断言,不伪造服务端内容。
|
||||
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 gated = await page.getByText('修改密码').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 openDocument(page: Page) {
|
||||
await page.locator('.tsb-tool', { hasText: '追溯审计' }).click()
|
||||
await expect(page.locator('.rdm-audit-project')).toBeVisible()
|
||||
await page.locator('.rdm-audit-project').selectOption({ label: MAIN })
|
||||
await page.getByRole('tab', { name: '资料与文档' }).click()
|
||||
const open = page.locator('.rdm-audit-tree-open').first()
|
||||
await expect(open).toBeVisible({ timeout: 10_000 })
|
||||
await open.click()
|
||||
await expect(page.locator('.drw')).toBeVisible()
|
||||
await expect(page.getByRole('tree', { name: '文档结构画布' })).toBeVisible({ timeout: 10_000 })
|
||||
}
|
||||
|
||||
const panel = (page: Page, label: string) =>
|
||||
page.locator('.drw-panel').filter({ has: page.locator('.ui-seclabel', { hasText: new RegExp(`^${label}$`) }) })
|
||||
|
||||
/** 失败文案必须可读:不得把原始 JSON 回执直出到主文案。 */
|
||||
async function expectReadableFailures(page: Page) {
|
||||
for (const msg of await page.locator('.drw-failure-msg').allTextContents()) {
|
||||
expect(msg.trim().startsWith('{')).toBe(false)
|
||||
expect(msg).not.toContain('"receipt"')
|
||||
}
|
||||
}
|
||||
|
||||
test.describe.serial('统一渲染 Task 11 文档阅读与交付', () => {
|
||||
test('结构画布与阅读视图共用选中与过滤,切换不丢', async ({ page }) => {
|
||||
await login(page)
|
||||
await openDocument(page)
|
||||
const nodes = page.locator('.drw-canvas-node')
|
||||
await expect(nodes.first()).toBeVisible()
|
||||
const count = await nodes.count()
|
||||
test.skip(count < 2, '种子文档无章节,跳过双视图选中断言')
|
||||
const target = nodes.nth(1)
|
||||
const title = (await target.getAttribute('title')) ?? ''
|
||||
await target.click()
|
||||
await expect(target).toHaveAttribute('aria-selected', 'true')
|
||||
await page.getByLabel('过滤节点').fill(title.slice(0, 2))
|
||||
await page.getByRole('tab', { name: '阅读视图' }).click()
|
||||
await expect(page.locator('.drw-reader')).toBeVisible()
|
||||
await expect(page.locator('.drw-reader-title[aria-current="true"]')).toHaveText(title)
|
||||
await expect(page.getByLabel('过滤节点')).toHaveValue(title.slice(0, 2))
|
||||
await page.getByRole('tab', { name: '结构画布' }).click()
|
||||
await expect(page.locator(`.drw-canvas-node[aria-selected="true"]`)).toHaveAttribute('title', title)
|
||||
})
|
||||
|
||||
test('结构画布可键盘导航并可滚动平移', async ({ page }) => {
|
||||
await login(page)
|
||||
await openDocument(page)
|
||||
const root = page.locator('.drw-canvas-node').first()
|
||||
await root.focus()
|
||||
await page.keyboard.press('ArrowDown')
|
||||
const selected = page.locator('.drw-canvas-node[aria-selected="true"]')
|
||||
if (await page.locator('.drw-canvas-node').count() > 1) {
|
||||
await expect(selected).toBeFocused()
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await expect(root).toHaveAttribute('aria-selected', 'true')
|
||||
}
|
||||
const canvas = page.getByRole('tree', { name: '文档结构画布' })
|
||||
const box = await canvas.boundingBox()
|
||||
expect(box?.height ?? 0).toBeGreaterThan(100)
|
||||
})
|
||||
|
||||
test('规则选择显示来源/版本/状态;不可用项给出原因;拒绝可读', async ({ page }) => {
|
||||
await login(page)
|
||||
await openDocument(page)
|
||||
const rules = panel(page, '渲染规则')
|
||||
await expect(rules.locator('.drw-profile').or(rules.locator('.drw-failure')).or(rules.getByText('没有可用的渲染规则')).first())
|
||||
.toBeVisible({ timeout: 10_000 })
|
||||
await expectReadableFailures(page)
|
||||
for (const option of await rules.locator('.drw-profile').all()) {
|
||||
await expect(option.locator('.drw-profile-source')).toHaveText(/文档规则|项目规则|模板规则|公司默认规则/)
|
||||
await expect(option.locator('.drw-profile-ver')).toHaveText(/^v.+/)
|
||||
if (await option.locator('input[disabled]').count()) await expect(option.locator('.drw-profile-reason')).not.toBeEmpty()
|
||||
}
|
||||
if (await rules.locator('.drw-notice').count()) await expect(rules.locator('.drw-notice')).toContainText('不会回退到公司默认规则')
|
||||
})
|
||||
|
||||
test('生成预览:状态与诊断可见,点击诊断选中对应节点', async ({ page }) => {
|
||||
await login(page)
|
||||
await openDocument(page)
|
||||
const generate = page.getByRole('button', { name: '生成预览', exact: true })
|
||||
await expect(generate).toBeVisible({ timeout: 10_000 })
|
||||
test.skip(await generate.isDisabled(), '无可预览规则')
|
||||
await generate.click()
|
||||
const preview = panel(page, '预览与提交')
|
||||
await expect(preview.locator('.drw-receipt').or(preview.locator('.drw-failure')).first()).toBeVisible({ timeout: 30_000 })
|
||||
await expectReadableFailures(page)
|
||||
if (await preview.locator('.drw-failure').count()) {
|
||||
await expect(preview.getByRole('button', { name: '重试' })).toBeVisible()
|
||||
return
|
||||
}
|
||||
const located = preview.locator('button.drw-diag-node').first()
|
||||
if (await located.count()) {
|
||||
const title = await located.getAttribute('title')
|
||||
await located.click()
|
||||
await expect(page.locator('.drw-canvas-node[aria-selected="true"], .drw-reader-title[aria-current="true"]').first())
|
||||
.toContainText(title ?? '')
|
||||
}
|
||||
await expect(page.locator('.drw-delivery').first()).toBeVisible({ timeout: 15_000 })
|
||||
})
|
||||
|
||||
test('交付记录可下载,文件名截断且 title 保留全名', async ({ page }) => {
|
||||
await login(page)
|
||||
await openDocument(page)
|
||||
const download = page.locator('.drw-download').first()
|
||||
test.skip(!(await download.waitFor({ state: 'visible', timeout: 10_000 }).then(() => true).catch(() => false)), '暂无交付记录')
|
||||
const file = page.waitForEvent('download')
|
||||
await download.click()
|
||||
const saved = await file
|
||||
expect(saved.suggestedFilename()).toMatch(/\.(docx|md|json|txt)$/)
|
||||
const name = panel(page, '交付记录').locator('.drw-filename').first()
|
||||
await expect(name).toHaveAttribute('title', saved.suggestedFilename())
|
||||
})
|
||||
|
||||
test('审阅:批注与修订分组,无法定位的单独成组', async ({ page }) => {
|
||||
await login(page)
|
||||
await openDocument(page)
|
||||
const reviewPanel = panel(page, '审阅')
|
||||
test.skip(!(await reviewPanel.count()), '服务未提供审阅能力')
|
||||
await expect(reviewPanel.getByText('暂无审阅提交').or(reviewPanel.locator('.drw-annos')).or(reviewPanel.getByText('暂无批注')).first())
|
||||
.toBeVisible({ timeout: 10_000 })
|
||||
await expectReadableFailures(page)
|
||||
await expect(reviewPanel).not.toContainText('<w:')
|
||||
})
|
||||
|
||||
test('窄屏与深色主题下保持可用', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await login(page)
|
||||
await page.evaluate(() => { document.documentElement.dataset.theme = 'dark' })
|
||||
await openDocument(page)
|
||||
const overflow = await page.locator('.drw').evaluate(el => el.scrollWidth - el.clientWidth)
|
||||
expect(overflow).toBeLessThanOrEqual(1)
|
||||
await expect(page.getByRole('button', { name: '← 返回文档树' })).toBeVisible()
|
||||
await page.getByRole('button', { name: '← 返回文档树' }).click()
|
||||
await expect(page.locator('.rdm-audit-tree-open').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('切换项目后不残留上一项目的文档内容', async ({ page }) => {
|
||||
await login(page)
|
||||
await openDocument(page)
|
||||
await page.locator('.rdm-audit-project').selectOption({ label: ISOLATED })
|
||||
await expect(page.locator('.drw')).toHaveCount(0)
|
||||
await page.getByRole('tab', { name: '资料与文档' }).click()
|
||||
await expect(page.getByText('暂无文档')).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user