test(e2e): full-system browser multi-user acceptance

This commit is contained in:
weichen
2026-09-26 03:48:34 +00:00
parent 4873c5e8b9
commit 466426a58f
19 changed files with 437 additions and 190 deletions
+60 -7
View File
@@ -250,7 +250,8 @@ async function adminCreateUser(
async function toggleCreateProjectMember(
projectModal: ReturnType<Page['locator']>,
displayName: string,
role?: 'reviewer' | 'viewer' | 'editor',
/** CreateProjectModal options: author|reviewer|approver|viewer (no editor). */
role?: 'author' | 'reviewer' | 'approver' | 'viewer',
) {
const memberName = projectModal.getByText(displayName, { exact: true }).first();
await expect(memberName).toBeVisible({ timeout: 15_000 });
@@ -264,6 +265,41 @@ async function toggleCreateProjectMember(
await memberRole.selectOption(role);
}
/** After create, promote a create-modal viewer to project_role=member (editable, no doc duty). */
async function promoteToProjectMember(page: Page, displayName: string) {
// LeftRail: title="管理当前项目成员与职责分配" (avoid matching vault "保存" buttons).
const openBtn = page.locator('button[title="管理当前项目成员与职责分配"]').first();
await expect(openBtn).toBeVisible({ timeout: 15_000 });
await openBtn.click();
const heading = page.getByText('项目成员与职责权限管理', { exact: true });
await expect(heading).toBeVisible({ timeout: 20_000 });
const modal = heading.locator('xpath=ancestor::div[contains(@style,"position: fixed")][1]');
// Row is flex space-between with the display name + a role <select>.
const nameEl = modal.getByText(displayName, { exact: true }).first();
await expect(nameEl).toBeVisible({ timeout: 15_000 });
const row = nameEl.locator(
'xpath=ancestor::div[contains(@style,"justify-content: space-between")][1]',
);
const roleSelect = row.locator('select').first();
await expect(roleSelect).toBeVisible({ timeout: 15_000 });
const opts = await roleSelect.locator('option').evaluateAll((nodes) =>
nodes.map((n) => ({
value: (n as HTMLOptionElement).value,
text: (n.textContent || '').trim(),
})),
);
const memberOpt = opts.find((o) => o.value === 'member' || /^项目成员/.test(o.text));
if (!memberOpt) {
fail(`promoteToProjectMember: no member option in ${JSON.stringify(opts)}`);
}
// onChange persists immediately — no separate save; do NOT match vault「保存」.
await roleSelect.selectOption(memberOpt.value);
await expect(roleSelect).toHaveValue('member');
await expect(row.getByText('项目成员 (Member)', { exact: true })).toBeVisible({ timeout: 15_000 });
await modal.getByRole('button', { name: '完成并关闭' }).click();
await expect(heading).toBeHidden({ timeout: 15_000 });
}
async function goToWorkflowDutyStep(page: Page) {
const approval = page.locator('label', { hasText: '纳入人工审批' }).first();
const selectHint = page.getByText('选择要激活的已发布工作流版本');
@@ -617,9 +653,9 @@ test.describe.serial('full-system browser multi-user E2E', () => {
if ((await desc.count()) === 0) fail('project form missing description');
await desc.fill('全系统浏览器 E2E:骨科超声吸引止血,覆盖多人协作与审签链路。');
await toggleCreateProjectMember(modal, USERS.reviewer.display, 'reviewer');
await toggleCreateProjectMember(modal, USERS.approver.display, 'viewer');
await toggleCreateProjectMember(modal, USERS.member.display, 'editor');
// guest optional — if listed, add as viewer
await toggleCreateProjectMember(modal, USERS.approver.display, 'approver');
// Plain collaborator: create-modal has no "member" option — select (defaults viewer) then promote.
await toggleCreateProjectMember(modal, USERS.member.display);
const guestVisible = await modal.getByText(USERS.guest.display, { exact: true }).count();
if (guestVisible > 0) {
await toggleCreateProjectMember(modal, USERS.guest.display, 'viewer');
@@ -628,14 +664,31 @@ test.describe.serial('full-system browser multi-user E2E', () => {
await modal.getByRole('button', { name: /确认立项/ }).click();
await expect(heading).toBeHidden({ timeout: 15_000 });
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
try {
await promoteToProjectMember(page, USERS.member.display);
} catch (promoteErr) {
record({
id: 'FS-PROJ-01b',
name: 'promote member role',
priority: 'P0',
input: `promote ${USERS.member.display} to project member`,
expected: 'project_role=member',
actual: String(promoteErr),
verdict: 'FAIL',
durationMs: Date.now() - t0,
console_errors: consoleErrorsFor(label),
notes: 'create-modal 仅有 author/reviewer/approver/viewer;成员级可编辑需立项后改派',
});
throw promoteErr;
}
await shot(page, 'FS-PROJ-01-created');
record({
id: 'FS-PROJ-01',
name: 'create project + members',
priority: 'P0',
input: `project=${PROJECT_NAME}; members reviewer/approver/member(+guest)`,
expected: 'banner shows project; members selected',
actual: `created; guestListed=${guestVisible > 0}`,
input: `project=${PROJECT_NAME}; reviewer/approver duties + member promote(+guest)`,
expected: 'banner shows project; member is project_role=member',
actual: `created; guestListed=${guestVisible > 0}; member promoted`,
verdict: 'PASS',
durationMs: Date.now() - t0,
console_errors: consoleErrorsFor(label),