fix(e2e): add approver D for workflow 三权 without inviting C
系统管理员 name collides in member list; unique 审批人D keeps UI-12.
This commit is contained in:
@@ -182,10 +182,12 @@ const suffix = `${Date.now().toString(36)}`;
|
|||||||
const USER_A = `uia_${suffix}`;
|
const USER_A = `uia_${suffix}`;
|
||||||
const USER_B = `uib_${suffix}`;
|
const USER_B = `uib_${suffix}`;
|
||||||
const USER_C = `uic_${suffix}`;
|
const USER_C = `uic_${suffix}`;
|
||||||
|
const USER_D = `uid_${suffix}`;
|
||||||
const INIT_PASS = `Init-${suffix}9`;
|
const INIT_PASS = `Init-${suffix}9`;
|
||||||
const FINAL_PASS_A = `PassA-${suffix}9`;
|
const FINAL_PASS_A = `PassA-${suffix}9`;
|
||||||
const FINAL_PASS_B = `PassB-${suffix}9`;
|
const FINAL_PASS_B = `PassB-${suffix}9`;
|
||||||
const FINAL_PASS_C = `PassC-${suffix}9`;
|
const FINAL_PASS_C = `PassC-${suffix}9`;
|
||||||
|
const FINAL_PASS_D = `PassD-${suffix}9`;
|
||||||
const PROJECT_NAME = `UI验收-超声微创系统-${suffix}`;
|
const PROJECT_NAME = `UI验收-超声微创系统-${suffix}`;
|
||||||
|
|
||||||
async function login(page: Page, username: string, password: string) {
|
async function login(page: Page, username: string, password: string) {
|
||||||
@@ -234,8 +236,31 @@ function dutyRoleSelect(page: Page, placeholder: '作者' | '审核' | '批准')
|
|||||||
.first();
|
.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Activate workflow with first intake doc under human approval + full 三权. */
|
async function selectDutyMemberByName(
|
||||||
async function activateProjectWorkflow(page: Page) {
|
page: Page,
|
||||||
|
placeholder: '作者' | '审核' | '批准',
|
||||||
|
displayName: string,
|
||||||
|
) {
|
||||||
|
const sel = dutyRoleSelect(page, placeholder);
|
||||||
|
await expect(sel).toBeVisible({ timeout: 10_000 });
|
||||||
|
const option = sel.locator('option').filter({ hasText: displayName }).first();
|
||||||
|
if ((await option.count()) === 0) {
|
||||||
|
fail(`duty select ${placeholder} missing member ${displayName}`);
|
||||||
|
}
|
||||||
|
const value = await option.getAttribute('value');
|
||||||
|
if (!value) fail(`duty select ${placeholder} option ${displayName} has empty value`);
|
||||||
|
await sel.selectOption(value);
|
||||||
|
await expect(sel).toHaveValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate workflow with first intake doc under human approval + full 三权.
|
||||||
|
* Pass display names so UI-13 can rely on B as reviewer (not option-order lottery).
|
||||||
|
*/
|
||||||
|
async function activateProjectWorkflow(
|
||||||
|
page: Page,
|
||||||
|
duties: { author: string; reviewer: string; Approver: string },
|
||||||
|
) {
|
||||||
await page.getByRole('button', { name: '配置工作流' }).click();
|
await page.getByRole('button', { name: '配置工作流' }).click();
|
||||||
await goToWorkflowDutyStep(page);
|
await goToWorkflowDutyStep(page);
|
||||||
const toggles = page.locator('label', { hasText: '纳入人工审批' });
|
const toggles = page.locator('label', { hasText: '纳入人工审批' });
|
||||||
@@ -246,19 +271,9 @@ async function activateProjectWorkflow(page: Page) {
|
|||||||
}
|
}
|
||||||
const firstToggle = toggles.first().locator('input[type="checkbox"]');
|
const firstToggle = toggles.first().locator('input[type="checkbox"]');
|
||||||
if (!(await firstToggle.isChecked())) await firstToggle.check();
|
if (!(await firstToggle.isChecked())) await firstToggle.check();
|
||||||
const author = dutyRoleSelect(page, '作者');
|
await selectDutyMemberByName(page, '作者', duties.author);
|
||||||
await expect(author).toBeVisible({ timeout: 10_000 });
|
await selectDutyMemberByName(page, '审核', duties.reviewer);
|
||||||
const values = (
|
await selectDutyMemberByName(page, '批准', duties.approver);
|
||||||
await author
|
|
||||||
.locator('option')
|
|
||||||
.evaluateAll((nodes) => nodes.map((n) => (n as HTMLOptionElement).value))
|
|
||||||
).filter(Boolean);
|
|
||||||
if (values.length < 3) {
|
|
||||||
fail(`workflow activate needs ≥3 project members for 三权, got ${values.length}`);
|
|
||||||
}
|
|
||||||
await author.selectOption(values[0]);
|
|
||||||
await dutyRoleSelect(page, '审核').selectOption(values[1]);
|
|
||||||
await dutyRoleSelect(page, '批准').selectOption(values[2]);
|
|
||||||
await page.getByRole('button', { name: /校验并预览/ }).click();
|
await page.getByRole('button', { name: /校验并预览/ }).click();
|
||||||
await expect(page.getByText('激活预览')).toBeVisible({ timeout: 20_000 });
|
await expect(page.getByText('激活预览')).toBeVisible({ timeout: 20_000 });
|
||||||
await page.getByRole('button', { name: '确认激活' }).click();
|
await page.getByRole('button', { name: '确认激活' }).click();
|
||||||
@@ -273,7 +288,7 @@ async function toggleCreateProjectMember(
|
|||||||
displayName: string,
|
displayName: string,
|
||||||
role?: 'reviewer' | 'viewer' | 'editor',
|
role?: 'reviewer' | 'viewer' | 'editor',
|
||||||
) {
|
) {
|
||||||
const memberName = projectModal.getByText(displayName, { exact: true });
|
const memberName = projectModal.getByText(displayName, { exact: true }).first();
|
||||||
await expect(memberName).toBeVisible({ timeout: 15_000 });
|
await expect(memberName).toBeVisible({ timeout: 15_000 });
|
||||||
await memberName.locator('xpath=ancestor::div[contains(@style,"cursor: pointer")][1]').click();
|
await memberName.locator('xpath=ancestor::div[contains(@style,"cursor: pointer")][1]').click();
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
@@ -775,6 +790,7 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
|
|||||||
await adminCreateUser(page, USER_A, `编制人A-${suffix}`, INIT_PASS, 'role-tech-lead');
|
await adminCreateUser(page, USER_A, `编制人A-${suffix}`, INIT_PASS, 'role-tech-lead');
|
||||||
await adminCreateUser(page, USER_B, `协作者B-${suffix}`, INIT_PASS, 'role-engineer');
|
await adminCreateUser(page, USER_B, `协作者B-${suffix}`, INIT_PASS, 'role-engineer');
|
||||||
await adminCreateUser(page, USER_C, `无关人C-${suffix}`, INIT_PASS, 'role-guest');
|
await adminCreateUser(page, USER_C, `无关人C-${suffix}`, INIT_PASS, 'role-guest');
|
||||||
|
await adminCreateUser(page, USER_D, `审批人D-${suffix}`, INIT_PASS, 'role-engineer');
|
||||||
// Full-page AdminConsoleView (not the old fixed modal): exit via 返回工作台.
|
// Full-page AdminConsoleView (not the old fixed modal): exit via 返回工作台.
|
||||||
await page.getByRole('button', { name: '返回工作台' }).click();
|
await page.getByRole('button', { name: '返回工作台' }).click();
|
||||||
await expect(page.getByText('新建立项会话')).toBeVisible({ timeout: 15_000 });
|
await expect(page.getByText('新建立项会话')).toBeVisible({ timeout: 15_000 });
|
||||||
@@ -799,6 +815,12 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
|
|||||||
await changePasswordFirstLogin(page, INIT_PASS, FINAL_PASS_C);
|
await changePasswordFirstLogin(page, INIT_PASS, FINAL_PASS_C);
|
||||||
await logoutViaUI(page);
|
await logoutViaUI(page);
|
||||||
|
|
||||||
|
await page.fill('input[placeholder*="系统用户名"]', USER_D);
|
||||||
|
await page.fill('input[placeholder*="登录密码"]', INIT_PASS);
|
||||||
|
await page.click('button:has-text("进入医疗协同工作台")');
|
||||||
|
await changePasswordFirstLogin(page, INIT_PASS, FINAL_PASS_D);
|
||||||
|
await logoutViaUI(page);
|
||||||
|
|
||||||
writePhaseSecrets({ passA: FINAL_PASS_A, passB: FINAL_PASS_B, passC: FINAL_PASS_C });
|
writePhaseSecrets({ passA: FINAL_PASS_A, passB: FINAL_PASS_B, passC: FINAL_PASS_C });
|
||||||
writePhaseState({
|
writePhaseState({
|
||||||
projectName: PROJECT_NAME,
|
projectName: PROJECT_NAME,
|
||||||
@@ -811,7 +833,7 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
|
|||||||
id: 'UI-00',
|
id: 'UI-00',
|
||||||
name: 'admin creates A/B/C',
|
name: 'admin creates A/B/C',
|
||||||
input: 'admin UI create users + first-login credential reset',
|
input: 'admin UI create users + first-login credential reset',
|
||||||
actual: `users ${USER_A},${USER_B},${USER_C} ready`,
|
actual: `users ${USER_A},${USER_B},${USER_C},${USER_D} ready`,
|
||||||
networkLogs: logs.filter((l) => !/password|login/i.test(l.path)),
|
networkLogs: logs.filter((l) => !/password|login/i.test(l.path)),
|
||||||
durationMs: Date.now() - t0,
|
durationMs: Date.now() - t0,
|
||||||
verdict: 'PASS',
|
verdict: 'PASS',
|
||||||
@@ -833,9 +855,9 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
|
|||||||
const desc = projectModal.locator('textarea');
|
const desc = projectModal.locator('textarea');
|
||||||
if ((await desc.count()) === 0) fail('project form missing required description textarea');
|
if ((await desc.count()) === 0) fail('project form missing required description textarea');
|
||||||
await desc.fill('用于骨科微创手术的双频超声吸引与止血,需符合 GB 9706.1 与相关法规要求。');
|
await desc.fill('用于骨科微创手术的双频超声吸引与止血,需符合 GB 9706.1 与相关法规要求。');
|
||||||
// B = reviewer; 系统管理员 = third member so workflow 三权 can activate (C stays non-member for UI-12).
|
// B = reviewer; D = third member for workflow 三权 (C stays non-member for UI-12).
|
||||||
await toggleCreateProjectMember(projectModal, `协作者B-${suffix}`, 'reviewer');
|
await toggleCreateProjectMember(projectModal, `协作者B-${suffix}`, 'reviewer');
|
||||||
await toggleCreateProjectMember(projectModal, '系统管理员', 'viewer');
|
await toggleCreateProjectMember(projectModal, `审批人D-${suffix}`, 'viewer');
|
||||||
await projectModal.getByRole('button', { name: /确认立项/ }).click();
|
await projectModal.getByRole('button', { name: /确认立项/ }).click();
|
||||||
await expect(projectModalHeading).toBeHidden({ timeout: 15_000 });
|
await expect(projectModalHeading).toBeHidden({ timeout: 15_000 });
|
||||||
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
|
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
|
||||||
|
|||||||
Reference in New Issue
Block a user