// Real React/Picker interaction with deterministic HTTP responses; no live account or database. import { test, expect, type Page } from '@playwright/test' test.skip(!process.env.PLAYWRIGHT_BASE_URL, 'set PLAYWRIGHT_BASE_URL to an isolated frontend server') function deferred() { let resolve!: () => void const promise = new Promise(ok => { resolve = ok }) return { promise, resolve } } async function dutiesPage(page: Page) { const writes: { command: string; params: Record }[] = [] const calls: { command: string; params: Record }[] = [] const holds = new Map; release: ReturnType }>() const active = new Set() const submissions: Record = { 'doc-A': null, 'doc-B': null } const commands = ['rdm.project.member.list', 'rdm.project.member.add', 'rdm.project.member.remove', 'rdm.duty.catalog', 'rdm.duty.get', 'rdm.review.assign.doc', 'rdm.review.assign.doc.batch', 'rdm.review.doc.submit', 'rdm.review.doc.approve', 'rdm.review.doc.reject', 'rdm.review.doc.final', 'rdm.review.doc.status'] const docs = [ { docId: 'doc-A', title: '文档A', typeKey: 'design', sections: 1, assigned: true, author: 'alice', reviewers: ['carol'], approver: 'dave' }, { docId: 'doc-B', title: '文档B', typeKey: 'design', sections: 1, assigned: true, author: 'bob', reviewers: ['carol'], approver: 'dave' }, ] await page.route('**/api/**', async route => { const request = route.request() const path = new URL(request.url()).pathname let body: unknown = [] if (path === '/api/health') body = { ok: true } else if (path === '/api/me') body = { owner: 'alice', name: 'Alice', auth: true, isAdmin: true } else if (path === '/api/projects') body = [{ id: 'host-1', name: '职责测试项目', createdAt: '', updatedAt: '', settings: { 'rdm.projectId': 'domain-7', 'rdm.state': 'ready' } }] else if (path === '/api/settings') body = { schema: [], defaults: {}, user: {}, project: {} } else if (path === '/api/commands') body = commands.map(id => ({ id, enabled: true })) else if (path.startsWith('/api/commands/')) { const command = decodeURIComponent(path.substring('/api/commands/'.length)) const params = request.postDataJSON().params as Record expect(params.projectId).toBe('domain-7') calls.push({ command, params }) const hold = holds.get(`${command}:${params.docId ?? ''}`) if (hold) { hold.started.resolve(); await hold.release.promise } let payload: unknown = {} if (command === 'rdm.duty.catalog') payload = { docs } else if (command === 'rdm.project.member.list') payload = { members: ['alice', 'bob', 'carol', 'dave'].map(owner => ({ owner, role: 'member', addedBy: '', addedAt: '' })) } else if (command === 'rdm.duty.get') { const doc = docs.find(d => d.docId === params.docId)! payload = { docId: doc.docId, title: doc.title, assigned: doc.assigned, roles: { author: doc.author, reviewers: doc.reviewers, approver: doc.approver }, sections: [], active: active.has(doc.docId) ? { round: 1, state: 'submitted', createdBy: 'alice' } : null } } else if (command === 'rdm.review.doc.status') payload = { submission: submissions[String(params.docId)] } else { writes.push({ command, params }) } body = { payload } } else if (path.endsWith('/cards')) body = { cards: [] } await route.fulfill({ contentType: 'application/json', body: JSON.stringify(body) }) }) await page.goto('/') await page.getByRole('button', { name: '项目设置', exact: true }).click() await page.getByRole('tab', { name: /文档职责/ }).click() await expect(page.getByRole('button', { name: '文档A', exact: true })).toBeVisible() return { writes, calls, active, submissions, hold(command: string, docId: string) { const pending = { started: deferred(), release: deferred() } holds.set(`${command}:${docId}`, pending) return pending } } } async function chooseMember(page: Page, label: string, owner: string) { await page.locator(`.picker[aria-label="${label}"]`).getByRole('combobox').fill(owner) await page.getByRole('option', { name: new RegExp(`^${owner}`) }).click() } async function settleResponse(page: Page, command: string, release: () => void) { const response = page.waitForResponse(r => r.url().endsWith(`/api/commands/${command}`)) release() await response await page.evaluate(() => new Promise(done => requestAnimationFrame(() => requestAnimationFrame(() => done())))) } test('a newly assigned document can submit its first whole-document review', async ({ page }) => { const fixture = await dutiesPage(page) await page.getByRole('button', { name: '文档A', exact: true }).click() await expect(page.getByText('该文档尚无整文提交记录(当前职责仍可分配)。', { exact: true })).toBeVisible() const submit = page.getByRole('button', { name: '整文提交(编制人)', exact: true }) await expect(submit).toBeEnabled() await submit.click() await expect(page.getByText('整文已提交审核(全文锁定)', { exact: true })).toBeVisible() expect(fixture.writes).toEqual([{ command: 'rdm.review.doc.submit', params: { projectId: 'domain-7', docId: 'doc-A' } }]) }) test('viewing active duties is passive; only a dirty save asks for confirmation', async ({ page }) => { const fixture = await dutiesPage(page) fixture.active.add('doc-A') const dialogs: string[] = [] let accept = false page.on('dialog', async dialog => { dialogs.push(dialog.message()); accept ? await dialog.accept() : await dialog.dismiss() }) await page.getByRole('button', { name: '文档A', exact: true }).click() await expect(page.getByRole('heading', { name: /文档A · 当前职责/ })).toBeVisible() const save = page.getByRole('button', { name: '保存当前职责', exact: true }) await expect(save).toBeDisabled() expect(dialogs).toEqual([]) await chooseMember(page, '编制人', 'bob') await expect(save).toBeEnabled() expect(dialogs).toEqual([]) await save.click() expect(dialogs).toHaveLength(1) expect(fixture.writes).toEqual([]) accept = true await save.click() await expect(page.getByText('已保存当前职责', { exact: true })).toBeVisible() expect(dialogs).toHaveLength(2) expect(fixture.writes).toEqual([{ command: 'rdm.review.assign.doc', params: { projectId: 'domain-7', docId: 'doc-A', author: 'bob', reviewers: ['carol'], approver: 'dave', } }]) }) test('a late document detail cannot overwrite another selection or its saved roles', async ({ page }) => { const fixture = await dutiesPage(page) const old = fixture.hold('rdm.duty.get', 'doc-A') await page.getByRole('button', { name: '文档A', exact: true }).click() await old.started.promise await page.getByRole('button', { name: '文档B', exact: true }).click() await expect(page.getByRole('heading', { name: '文档B · 当前职责', exact: true })).toBeVisible() await settleResponse(page, 'rdm.duty.get', old.release.resolve) await expect(page.getByRole('heading', { name: '文档B · 当前职责', exact: true })).toBeVisible() expect(fixture.calls.filter(c => c.command === 'rdm.review.doc.status' && c.params.docId === 'doc-A')).toEqual([]) await chooseMember(page, '批准人', 'alice') await page.getByRole('button', { name: '保存当前职责', exact: true }).click() await expect(page.getByText('已保存当前职责', { exact: true })).toBeVisible() expect(fixture.writes).toEqual([{ command: 'rdm.review.assign.doc', params: { projectId: 'domain-7', docId: 'doc-B', author: 'bob', reviewers: ['carol'], approver: 'alice', } }]) }) test('a late review status cannot replace the selected document frozen review', async ({ page }) => { const fixture = await dutiesPage(page) const submission = (round: number, author: string) => ({ round, state: 'submitted', digest: '', createdBy: author, createdAt: '', duties: { author, reviewers: ['carol'], approver: 'dave' }, reviews: {}, sections: {} }) fixture.submissions['doc-A'] = submission(1, 'alice') fixture.submissions['doc-B'] = submission(2, 'bob') const old = fixture.hold('rdm.review.doc.status', 'doc-A') await page.getByRole('button', { name: '文档A', exact: true }).click() await old.started.promise await page.getByRole('button', { name: '文档B', exact: true }).click() await expect(page.getByRole('heading', { name: /整文审签 · 第 2 轮/ })).toBeVisible() await settleResponse(page, 'rdm.review.doc.status', old.release.resolve) await expect(page.getByRole('heading', { name: /整文审签 · 第 2 轮/ })).toBeVisible() await expect(page.getByRole('heading', { name: /整文审签 · 第 1 轮/ })).toHaveCount(0) }) test('closing the project panel invalidates a pending detail before its follow-up status read', async ({ page }) => { const fixture = await dutiesPage(page) const old = fixture.hold('rdm.duty.get', 'doc-A') await page.getByRole('button', { name: '文档A', exact: true }).click() await old.started.promise await page.getByRole('dialog').getByRole('button', { name: '关闭', exact: true }).click() await expect(page.getByRole('tab', { name: /文档职责/ })).toHaveCount(0) await settleResponse(page, 'rdm.duty.get', old.release.resolve) expect(fixture.calls.filter(c => c.command === 'rdm.review.doc.status' && c.params.docId === 'doc-A')).toEqual([]) })