import type { APIRequestContext, Locator, Page } from '@playwright/test' import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures' import { adminAccessToken, chooseSelectOption, confirmMessageBox, envelopeData, fillFormItem, loginAsAdmin, tableRowByText, visibleDialog, } from './helpers' test.describe.configure({ mode: 'serial' }) test.use({ trace: 'off', video: 'off' }) const suffix = runPrefix.replace(/[^A-Za-z0-9]/g, '').slice(-14).toLowerCase() const data = { departmentName: `${runPrefix}-测试部门`, departmentCode: `E2E_${suffix.toUpperCase()}`.slice(0, 32), roleName: `${runPrefix}-测试角色`, roleCode: `e2e_${suffix}`.slice(0, 48), username: `e2e_${suffix}`.slice(0, 32), displayName: `${runPrefix}-学员`, editedDisplayName: `${runPrefix}-已编辑学员`, initialPassword: `Init@${suffix.slice(-6)}Aa1`, finalPassword: `Final@${suffix.slice(-5)}Bb2`, } const auth = (token: string) => ({ Authorization: `Bearer ${token}` }) const record = (value: unknown) => value && typeof value === 'object' ? value as Record : {} const records = (value: unknown) => { const root = record(envelopeData(value)) const items = root.records ?? root.items return Array.isArray(items) ? items.map(record) : Array.isArray(envelopeData(value)) ? (envelopeData(value) as unknown[]).map(record) : [] } async function json(response: Awaited>) { return response.json().catch(() => ({})) } async function cleanupCreatedData(request: APIRequestContext, token: string) { const result: Record = {} const userList = await request.get('/api/auth/v1/users', { headers: auth(token), params: { keyword: data.username, size: 100 } }) const user = records(await json(userList)).find((item) => String(item.username) === data.username) if (user) { const response = await request.delete(`/api/auth/v1/users/${String(user.id)}`, { headers: auth(token), data: { version: Number(user.version ?? user.dataVersion ?? 0) }, }) result.user = `${response.status()}` } const roleList = await request.get('/api/auth/v1/roles', { headers: auth(token), params: { keyword: data.roleCode, size: 100 } }) const role = records(await json(roleList)).find((item) => String(item.code) === data.roleCode) if (role) { const response = await request.delete(`/api/auth/v1/roles/${String(role.id)}`, { headers: auth(token), data: { version: Number(role.version ?? role.dataVersion ?? 0) }, }) result.role = `${response.status()}` } const departmentsResponse = await request.get('/api/auth/v1/departments', { headers: auth(token) }) const departmentValue = envelopeData(await json(departmentsResponse)) const departments = Array.isArray(departmentValue) ? departmentValue.map(record) : [] const department = departments.find((item) => String(item.code) === data.departmentCode) if (department) { const response = await request.post(`/api/auth/v1/departments/${String(department.id)}/migrate-and-delete`, { headers: auth(token), data: { version: Number(department.version ?? department.dataVersion ?? 0) }, }) result.department = `${response.status()}` } return result } async function fillInputByPlaceholder(page: Page, placeholder: string, value: string) { const input = page.getByPlaceholder(placeholder) await expect(input).toBeVisible() await input.fill(value) } async function logout(page: Page) { await page.locator('.user-menu-trigger').click() await page.getByRole('menuitem', { name: /退出登录/ }).click() await confirmMessageBox(page, '退出登录') await expect(page).toHaveURL(/\/login(?:\?|$)/) } async function selectStudentIdentity(page: Page) { await page.locator('[data-role-code="student"]').click() const picker = page.locator('.login-identity-picker') await expect(picker).toBeVisible() await picker.click() const option = page.locator('.login-identity-popper .el-select-dropdown__item').filter({ hasText: data.editedDisplayName }).first() await expect(option).toBeVisible() await option.click() } test('IAM 真实生命周期:部门、角色、用户、授权、首次改密和逆序清理', async ({ page, request }, testInfo) => { test.slow() const token = await adminAccessToken(request) await cleanupCreatedData(request, token) await loginAsAdmin(page) let primaryError: unknown let cleanup: Record = {} try { await test.step('通过部门管理页面创建并持久化测试部门', async () => { await page.goto('/organization/departments') await page.getByRole('button', { name: '新增部门', exact: true }).click() const dialog = visibleDialog(page) await fillFormItem(dialog, '部门名称', data.departmentName) await fillFormItem(dialog, '部门编码', data.departmentCode) await fillFormItem(dialog, '部门职责', `${runPrefix} E2E 自动化部门,运行结束后删除`) await dialog.getByRole('button', { name: '创建部门' }).click() await fillInputByPlaceholder(page, '请输入部门、编码或负责人', data.departmentCode) await page.getByRole('button', { name: '搜索', exact: true }).click() await expect(tableRowByText(page, data.departmentName)).toBeVisible() await captureScreenshot(page, testInfo, '01-department-created') }) await test.step('通过角色管理页面创建、编辑并持久化自定义角色', async () => { await page.goto('/organization/roles') await page.getByRole('button', { name: '新增角色', exact: true }).click() const dialog = visibleDialog(page) await fillFormItem(dialog, '角色名称', data.roleName) await fillFormItem(dialog, '角色编码', data.roleCode) await fillFormItem(dialog, '界面简称', '测') await fillFormItem(dialog, '角色说明', `${runPrefix} 创建测试`) await dialog.getByRole('button', { name: '创建角色' }).click() await fillInputByPlaceholder(page, '请输入角色名称、编码或说明', data.roleCode) await page.getByRole('button', { name: '搜索', exact: true }).click() const row = tableRowByText(page, data.roleName) await expect(row).toBeVisible() await row.getByRole('button', { name: '编辑' }).click() const editDialog = visibleDialog(page) await fillFormItem(editDialog, '角色说明', `${runPrefix} 已编辑并保存`) await editDialog.getByRole('button', { name: '保存修改' }).click() await expect(tableRowByText(page, '已编辑并保存')).toBeVisible() await captureScreenshot(page, testInfo, '02-role-created-edited') }) await test.step('在权限页给临时角色保存一项真实权限', async () => { await page.goto('/organization/permissions') const roleButton = page.locator('.role-selector > button').filter({ hasText: data.roleName }) await expect(roleButton).toBeVisible() await roleButton.click() const uncheckedPermission = page.locator('.permission-item[aria-checked="false"]').first() await expect(uncheckedPermission).toBeVisible() await uncheckedPermission.click() const save = page.getByRole('button', { name: '保存权限配置' }) await expect(save).toBeEnabled() await save.click() await expect(page.getByText(/权限已保存并立即生效/).last()).toBeVisible() await captureScreenshot(page, testInfo, '03-permission-saved') }) await test.step('通过用户管理页面创建学员账号并编辑姓名', async () => { await page.goto('/organization/users') await page.getByRole('button', { name: '新增用户', exact: true }).click() const dialog = visibleDialog(page) await fillFormItem(dialog, '姓名', data.displayName) await fillFormItem(dialog, '登录账号', data.username) await chooseSelectOption(page, dialog, '所属部门', data.departmentName) await fillFormItem(dialog, '初始密码', data.initialPassword) const studentRole = dialog.locator('.el-checkbox').filter({ hasText: '学员' }).first() await expect(studentRole).toBeVisible() await studentRole.click() await chooseSelectOption(page, dialog, '登录默认身份', '学员') await dialog.getByRole('button', { name: '创建用户' }).click() await fillInputByPlaceholder(page, '请输入姓名、账号、部门或角色', data.username) await page.getByRole('button', { name: '搜索', exact: true }).click() const row = tableRowByText(page, data.username) await expect(row).toBeVisible() await row.getByRole('button', { name: '编辑' }).click() const editDialog = visibleDialog(page) await fillFormItem(editDialog, '姓名', data.editedDisplayName) await editDialog.getByRole('button', { name: '保存修改' }).click() await expect(tableRowByText(page, data.editedDisplayName)).toBeVisible() await captureScreenshot(page, testInfo, '04-user-created-edited') }) await test.step('学员按身份选择登录并完成首次强制改密', async () => { await logout(page) await selectStudentIdentity(page) await page.locator('input[name="password"]').fill(data.initialPassword) await page.getByRole('button', { name: '登录系统' }).click() await expect(page).toHaveURL(/\/change-password(?:\?|$)/) await expect(page.getByRole('heading', { name: '设置新密码' })).toBeVisible() await page.getByPlaceholder('请输入新密码').fill(data.finalPassword) await page.getByPlaceholder('请再次输入').fill(data.finalPassword) await captureScreenshot(page, testInfo, '05-initial-password-form', [page.locator('input[type="password"]')]) await page.getByRole('button', { name: '保存并进入平台' }).click() await expect(page).toHaveURL(/\/overview(?:\?|$)/) await expect(page.locator('.user-menu-trigger')).toContainText(data.editedDisplayName) await captureScreenshot(page, testInfo, '06-student-login-success') await page.goto('/organization/users') await expect(page).toHaveURL(/\/overview\?forbidden=1$/) await expect(page.getByRole('heading', { name: '用户管理' })).toHaveCount(0) await captureScreenshot(page, testInfo, '07-student-system-route-forbidden') await logout(page) await loginAsAdmin(page) }) await test.step('列表筛选、停启入口及重置密码两种模式可以交互', async () => { await page.goto('/organization/users') await fillInputByPlaceholder(page, '请输入姓名、账号、部门或角色', data.username) await page.getByRole('button', { name: '搜索', exact: true }).click() const row = tableRowByText(page, data.username) await row.getByRole('button', { name: '重置密码' }).click() const dialog = visibleDialog(page) await expect(dialog.getByRole('radio', { name: /系统重置/ })).toBeChecked() await dialog.getByText('手动重置', { exact: true }).click() await expect(dialog.getByPlaceholder('请输入符合全部规则的新密码')).toBeVisible() await captureScreenshot(page, testInfo, '07-password-reset-modes', [dialog.locator('input[type="password"]')]) await dialog.getByRole('button', { name: '取消' }).click() }) } catch (error) { primaryError = error } finally { cleanup = await cleanupCreatedData(request, token).catch((error) => ({ cleanupError: String(error) })) await attachJson(testInfo, 'cleanup-result', cleanup) } if (primaryError) throw primaryError expect(cleanup.user, '用户应完成清理').toMatch(/^2/) expect(cleanup.role, '角色应完成清理').toMatch(/^2/) expect(cleanup.department, '部门应完成清理').toMatch(/^2/) })