|
- import type { APIRequestContext, APIResponse, 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(-12).toLowerCase()
- const data = {
- parentName: `${runPrefix}-组织父部门`,
- editedParentName: `${runPrefix}-已编辑父部门`,
- parentCode: `APE2E_P_${suffix}`.toUpperCase().slice(0, 32),
- childName: `${runPrefix}-组织子部门`,
- childCode: `APE2E_C_${suffix}`.toUpperCase().slice(0, 32),
- grandchildName: `${runPrefix}-组织孙部门`,
- grandchildCode: `APE2E_G_${suffix}`.toUpperCase().slice(0, 32),
- roleName: `${runPrefix}-范围角色`,
- roleCode: `ape2e_scope_${suffix}`.slice(0, 48),
- username: `ape2e_state_${suffix}`.slice(0, 32),
- displayName: `${runPrefix}-状态学员`,
- editedDisplayName: `${runPrefix}-已编辑状态学员`,
- initialPassword: `Init@${suffix.slice(-6)}Aa1`,
- }
-
- type ApiRecord = Record<string, unknown>
-
- const auth = (token: string) => ({ Authorization: `Bearer ${token}` })
- const record = (value: unknown): ApiRecord => value && typeof value === 'object' ? value as ApiRecord : {}
- const versionOf = (value: ApiRecord) => Number(value.version ?? value.dataVersion ?? 0)
-
- async function responseJson(response: APIResponse) {
- return response.json().catch(() => ({}))
- }
-
- function listRecords(value: unknown): ApiRecord[] {
- const payload = envelopeData(value)
- if (Array.isArray(payload)) return payload.map(record)
- const root = record(payload)
- const source = root.records ?? root.items ?? root.departments ?? root.roles ?? root.users
- return Array.isArray(source) ? source.map(record) : []
- }
-
- async function apiDepartments(request: APIRequestContext, token: string) {
- const response = await request.get('/api/auth/v1/departments', { headers: auth(token) })
- expect(response.ok(), `读取部门失败:HTTP ${response.status()}`).toBeTruthy()
- return listRecords(await responseJson(response))
- }
-
- async function apiDepartment(request: APIRequestContext, token: string, code: string) {
- return (await apiDepartments(request, token)).find((item) => String(item.code) === code)
- }
-
- async function apiRole(request: APIRequestContext, token: string) {
- const response = await request.get('/api/auth/v1/roles', {
- headers: auth(token),
- params: { keyword: data.roleCode, page: 1, size: 100 },
- })
- expect(response.ok(), `读取角色失败:HTTP ${response.status()}`).toBeTruthy()
- return listRecords(await responseJson(response)).find((item) => String(item.code) === data.roleCode)
- }
-
- async function apiUser(request: APIRequestContext, token: string) {
- const response = await request.get('/api/auth/v1/users', {
- headers: auth(token),
- params: { keyword: data.username, page: 1, size: 100 },
- })
- expect(response.ok(), `读取用户失败:HTTP ${response.status()}`).toBeTruthy()
- return listRecords(await responseJson(response)).find((item) => String(item.username) === data.username)
- }
-
- async function apiRolePermissions(request: APIRequestContext, token: string, roleId: string) {
- const response = await request.get(`/api/auth/v1/permissions/roles/${roleId}`, { headers: auth(token) })
- expect(response.ok(), `读取角色权限失败:HTTP ${response.status()}`).toBeTruthy()
- const payload = record(envelopeData(await responseJson(response)))
- const source = payload.permissions ?? payload.permissionCodes ?? payload.codes
- return Array.isArray(source) ? source.map(String) : []
- }
-
- async function cleanupCreatedData(request: APIRequestContext, token: string) {
- const result: Record<string, string> = {}
- const user = await apiUser(request, token)
- if (user) {
- const response = await request.delete(`/api/auth/v1/users/${String(user.id)}`, {
- headers: auth(token), data: { version: versionOf(user) },
- })
- result.user = String(response.status())
- } else result.user = 'absent'
-
- const role = await apiRole(request, token)
- if (role) {
- const response = await request.delete(`/api/auth/v1/roles/${String(role.id)}`, {
- headers: auth(token), data: { version: versionOf(role) },
- })
- result.role = String(response.status())
- } else result.role = 'absent'
-
- for (const [label, code] of [
- ['grandchild', data.grandchildCode],
- ['child', data.childCode],
- ['parent', data.parentCode],
- ] as const) {
- const department = await apiDepartment(request, token, code)
- if (!department) {
- result[label] = 'absent'
- continue
- }
- const response = await request.post(`/api/auth/v1/departments/${String(department.id)}/migrate-and-delete`, {
- headers: auth(token), data: { version: versionOf(department) },
- })
- result[label] = String(response.status())
- }
- return result
- }
-
- async function search(page: Page, placeholder: string, keyword: string) {
- const input = page.getByPlaceholder(placeholder)
- await expect(input).toBeVisible()
- await input.fill(keyword)
- await page.getByRole('button', { name: '搜索', exact: true }).click()
- }
-
- async function openDepartment(page: Page, code: string) {
- await search(page, '请输入部门、编码或负责人', code)
- const row = tableRowByText(page, code)
- await expect(row).toBeVisible()
- return row
- }
-
- async function createDepartment(page: Page, parentCode: string | null, name: string, code: string) {
- if (parentCode) {
- const parentRow = await openDepartment(page, parentCode)
- await parentRow.getByRole('button', { name: '新增下级', exact: true }).click()
- } else {
- await page.getByRole('button', { name: '新增部门', exact: true }).click()
- }
- const dialog = visibleDialog(page)
- await fillFormItem(dialog, '部门名称', name)
- await fillFormItem(dialog, '部门编码', code)
- await fillFormItem(dialog, '部门职责', `${runPrefix} 页面提交的组织层级测试数据`)
- await dialog.getByRole('button', { name: '创建部门' }).click()
- await expect(dialog).toBeHidden()
- await expect(await openDepartment(page, code)).toContainText(name)
- }
-
- 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()
- }
-
- async function clearBrowserSession(page: Page) {
- await page.evaluate(() => {
- window.localStorage.clear()
- window.sessionStorage.clear()
- })
- await page.context().clearCookies()
- }
-
- test('IAM 扩展页面闭环:层级迁移、范围授权、停启登录与页面删除', async ({ page, request }, testInfo) => {
- test.slow()
- const token = await adminAccessToken(request)
- await cleanupCreatedData(request, token)
- await loginAsAdmin(page)
- let primaryError: unknown
-
- try {
- await test.step('父子孙部门全部页面创建,父部门编辑后刷新与 API 均持久化', async () => {
- await page.goto('/organization/departments')
- await createDepartment(page, null, data.parentName, data.parentCode)
- await createDepartment(page, data.parentCode, data.childName, data.childCode)
- await createDepartment(page, data.childCode, data.grandchildName, data.grandchildCode)
-
- const parentRow = await openDepartment(page, data.parentCode)
- await parentRow.getByRole('button', { name: '编辑', exact: true }).click()
- const dialog = visibleDialog(page)
- await fillFormItem(dialog, '部门名称', data.editedParentName)
- await fillFormItem(dialog, '部门职责', `${runPrefix} 已通过页面编辑并保存`)
- await dialog.getByRole('button', { name: '保存修改' }).click()
- await expect(dialog).toBeHidden()
- await page.reload()
- await expect(await openDepartment(page, data.parentCode)).toContainText(data.editedParentName)
-
- const parent = await apiDepartment(request, token, data.parentCode)
- const child = await apiDepartment(request, token, data.childCode)
- const grandchild = await apiDepartment(request, token, data.grandchildCode)
- expect(parent?.name).toBe(data.editedParentName)
- expect(String(child?.parentId)).toBe(String(parent?.id))
- expect(String(grandchild?.parentId)).toBe(String(child?.id))
- await captureScreenshot(page, testInfo, '01-department-hierarchy-edited-and-reloaded')
- })
-
- await test.step('含启用孙部门的子部门页面级联停用,并逐级页面启用', async () => {
- const childRow = await openDepartment(page, data.childCode)
- await childRow.getByRole('button', { name: '停用', exact: true }).click()
- await confirmMessageBox(page, '级联停用')
- await expect(await openDepartment(page, data.childCode)).toContainText('已停用')
- expect((await apiDepartment(request, token, data.childCode))?.enabled).toBe(false)
- expect((await apiDepartment(request, token, data.grandchildCode))?.enabled).toBe(false)
-
- await (await openDepartment(page, data.childCode)).getByRole('button', { name: '启用', exact: true }).click()
- await expect(await openDepartment(page, data.childCode)).toContainText('已启用')
- await (await openDepartment(page, data.grandchildCode)).getByRole('button', { name: '启用', exact: true }).click()
- await expect(await openDepartment(page, data.grandchildCode)).toContainText('已启用')
- expect((await apiDepartment(request, token, data.childCode))?.enabled).toBe(true)
- expect((await apiDepartment(request, token, data.grandchildCode))?.enabled).toBe(true)
- await page.reload()
- await expect(await openDepartment(page, data.grandchildCode)).toContainText('已启用')
- await captureScreenshot(page, testInfo, '02-department-cascade-disabled-and-reenabled')
- })
-
- await test.step('角色通过页面创建后改为自定义数据范围并选择父部门,刷新与 API 持久化', async () => {
- await page.goto('/organization/roles')
- await page.getByRole('button', { name: '新增角色', exact: true }).click()
- let 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 search(page, '请输入角色名称、编码或说明', data.roleCode)
- let row = tableRowByText(page, data.roleCode)
- await expect(row).toBeVisible()
- await row.getByRole('button', { name: '编辑', exact: true }).click()
- dialog = visibleDialog(page)
- await chooseSelectOption(page, dialog, '数据范围', '自定义范围')
- await chooseSelectOption(page, dialog, '自定义部门范围', data.childName)
- await fillFormItem(dialog, '角色说明', `${runPrefix} CUSTOM 范围已保存`)
- await dialog.getByRole('button', { name: '保存修改' }).click()
- await page.reload()
- await search(page, '请输入角色名称、编码或说明', data.roleCode)
- row = tableRowByText(page, data.roleCode)
- await expect(row).toContainText('自定义范围')
- await expect(row).toContainText('CUSTOM 范围已保存')
-
- const role = await apiRole(request, token)
- const child = await apiDepartment(request, token, data.childCode)
- expect(role?.dataScope).toBe('CUSTOM')
- expect((role?.departmentScopeIds as unknown[]).map(String)).toEqual([String(child?.id)])
- await captureScreenshot(page, testInfo, '03-role-custom-scope-saved-and-reloaded')
- })
-
- await test.step('权限页完成整组全选、跨组多选和整组取消,两次保存刷新均与 API 一致', async () => {
- const row = tableRowByText(page, data.roleCode)
- await row.getByRole('button', { name: '配置权限', exact: true }).click()
- await expect(page).toHaveURL(/\/organization\/permissions\?role=/)
- const groups = page.locator('.permission-group')
- await expect(groups.first()).toBeVisible()
- expect(await groups.count()).toBeGreaterThan(1)
- const firstGroup = groups.nth(0)
- const secondGroup = groups.nth(1)
- const firstCodes = (await firstGroup.locator('.permission-item code').allTextContents()).map((item) => item.trim())
- const extraItem = secondGroup.locator('.permission-item').first()
- const extraCode = (await extraItem.locator('code').innerText()).trim()
- await firstGroup.getByRole('button', { name: '全选本组' }).click()
- await extraItem.click()
- await page.getByRole('button', { name: '保存权限配置' }).click()
- await expect(page.getByText(/权限已保存并立即生效/).last()).toBeVisible()
-
- const role = await apiRole(request, token)
- const firstSaved = await apiRolePermissions(request, token, String(role?.id))
- expect(firstSaved).toEqual(expect.arrayContaining([...firstCodes, extraCode]))
- await page.reload()
- await expect(page.locator('.role-selector > button.active')).toContainText(data.roleName)
- for (const code of [...firstCodes, extraCode]) {
- await expect(page.locator('.permission-item').filter({ hasText: code }).first()).toHaveAttribute('aria-checked', 'true')
- }
-
- await page.locator('.permission-group').first().getByRole('button', { name: '清空' }).click()
- await page.getByRole('button', { name: '保存权限配置' }).click()
- await expect(page.getByText(/权限已保存并立即生效/).last()).toBeVisible()
- const secondSaved = await apiRolePermissions(request, token, String(role?.id))
- expect(secondSaved).toContain(extraCode)
- firstCodes.forEach((code) => expect(secondSaved).not.toContain(code))
- await page.reload()
- await expect(page.locator('.permission-item').filter({ hasText: extraCode }).first()).toHaveAttribute('aria-checked', 'true')
- for (const code of firstCodes) {
- await expect(page.locator('.permission-item').filter({ hasText: code }).first()).toHaveAttribute('aria-checked', 'false')
- }
- await captureScreenshot(page, testInfo, '04-permission-group-save-clear-and-reload')
- })
-
- await test.step('用户页面创建并分配学员与自定义角色,编辑后刷新和 API 持久化', async () => {
- await page.goto('/organization/users')
- await page.getByRole('button', { name: '新增用户', exact: true }).click()
- let dialog = visibleDialog(page)
- await fillFormItem(dialog, '姓名', data.displayName)
- await fillFormItem(dialog, '登录账号', data.username)
- await chooseSelectOption(page, dialog, '所属部门', data.childName)
- await fillFormItem(dialog, '初始密码', data.initialPassword)
- await dialog.locator('.el-checkbox').filter({ hasText: '学员' }).first().click()
- await dialog.locator('.el-checkbox').filter({ hasText: data.roleName }).first().click()
- await chooseSelectOption(page, dialog, '登录默认身份', '学员')
- await dialog.getByRole('button', { name: '创建用户' }).click()
- await search(page, '请输入姓名、账号、部门或角色', data.username)
- let row = tableRowByText(page, data.username)
- await expect(row).toBeVisible()
- await row.getByRole('button', { name: '编辑', exact: true }).click()
- dialog = visibleDialog(page)
- await fillFormItem(dialog, '姓名', data.editedDisplayName)
- await dialog.getByRole('button', { name: '保存修改' }).click()
- await page.reload()
- await search(page, '请输入姓名、账号、部门或角色', data.username)
- row = tableRowByText(page, data.username)
- await expect(row).toContainText(data.editedDisplayName)
- const user = await apiUser(request, token)
- const role = await apiRole(request, token)
- expect(user?.displayName).toBe(data.editedDisplayName)
- expect((user?.roleIds as unknown[]).map(String)).toContain(String(role?.id))
- await captureScreenshot(page, testInfo, '05-user-created-edited-and-reloaded')
- })
-
- await test.step('有关联用户时角色页面删除禁用且正式 API 拒绝,角色页面停启可持久化', async () => {
- await page.goto('/organization/roles')
- await search(page, '请输入角色名称、编码或说明', data.roleCode)
- let row = tableRowByText(page, data.roleCode)
- await expect(row.getByRole('button', { name: '删除', exact: true })).toBeDisabled()
- let role = await apiRole(request, token)
- const blockedDelete = await request.delete(`/api/auth/v1/roles/${String(role?.id)}`, {
- headers: auth(token), data: { version: versionOf(role ?? {}) },
- })
- expect(blockedDelete.status()).toBe(409)
-
- await row.getByRole('button', { name: '停用', exact: true }).click()
- await confirmMessageBox(page, '确认')
- await expect(tableRowByText(page, data.roleCode)).toContainText('已停用')
- role = await apiRole(request, token)
- expect(role?.enabled).toBe(false)
- await page.reload()
- await search(page, '请输入角色名称、编码或说明', data.roleCode)
- row = tableRowByText(page, data.roleCode)
- await expect(row).toContainText('已停用')
- await row.getByRole('button', { name: '启用', exact: true }).click()
- await confirmMessageBox(page, '确认')
- await expect(tableRowByText(page, data.roleCode)).toContainText('已启用')
- expect((await apiRole(request, token))?.enabled).toBe(true)
- await page.reload()
- await search(page, '请输入角色名称、编码或说明', data.roleCode)
- await expect(tableRowByText(page, data.roleCode)).toContainText('已启用')
- await captureScreenshot(page, testInfo, '06-linked-role-delete-blocked-and-status-restored')
- })
-
- await test.step('删除子部门时页面迁移关联用户与直属孙部门,刷新树和 API 均无悬空引用', async () => {
- await page.goto('/organization/departments')
- const childRow = await openDepartment(page, data.childCode)
- await expect(childRow).toContainText('1')
- await childRow.getByRole('button', { name: '删除', exact: true }).click()
- const dialog = visibleDialog(page)
- await expect(dialog).toContainText('关联用户')
- await expect(dialog).toContainText('直属下级')
- const migrationSelects = dialog.locator('.el-form-item').filter({ hasText: /迁入部门|新上级/ }).locator('.el-select')
- await expect(migrationSelects).toHaveCount(2)
- await expect(migrationSelects.nth(0)).toContainText(data.editedParentName)
- await expect(migrationSelects.nth(1)).toContainText(data.editedParentName)
- await captureScreenshot(page, testInfo, '07-department-delete-migration-confirmation')
- await dialog.getByRole('button', { name: '确认删除并迁移' }).click()
- await expect(dialog).toBeHidden()
- await page.reload()
- await search(page, '请输入部门、编码或负责人', data.childCode)
- await expect(tableRowByText(page, data.childCode)).toHaveCount(0)
- await expect(page.locator('.department-scope-tree')).not.toContainText(data.childName)
- await expect(page.locator('.department-scope-tree')).toContainText(data.grandchildName)
-
- const parent = await apiDepartment(request, token, data.parentCode)
- const child = await apiDepartment(request, token, data.childCode)
- const grandchild = await apiDepartment(request, token, data.grandchildCode)
- const user = await apiUser(request, token)
- expect(child).toBeUndefined()
- expect(String(grandchild?.parentId)).toBe(String(parent?.id))
- expect(String(user?.departmentId)).toBe(String(parent?.id))
- const role = await apiRole(request, token)
- const migratedScopeIds = (role?.departmentScopeIds as unknown[]).map(String)
- expect(migratedScopeIds).toEqual([String(parent?.id)])
- await page.goto(`/organization/roles`)
- await search(page, '请输入角色名称、编码或说明', data.roleCode)
- await tableRowByText(page, data.roleCode).getByRole('button', { name: '编辑', exact: true }).click()
- const scopeDialog = visibleDialog(page)
- await expect(scopeDialog.getByText(data.editedParentName, { exact: true })).toBeVisible()
- await expect(scopeDialog.getByText(data.childName, { exact: true })).toHaveCount(0)
- await captureScreenshot(page, testInfo, '08-role-scope-migrated-without-deleted-department')
- await scopeDialog.getByRole('button', { name: '取消' }).click()
- await page.goto('/organization/departments')
- await search(page, '请输入部门、编码或负责人', data.grandchildCode)
- await expect(tableRowByText(page, data.grandchildCode)).toBeVisible()
- await captureScreenshot(page, testInfo, '09-department-migration-tree-and-table-reloaded')
- })
-
- await test.step('用户页面停用后登录 API 明确拒绝且身份选择器隐藏,启用后页面登录恢复', async () => {
- await page.goto('/organization/users')
- await search(page, '请输入姓名、账号、部门或角色', data.username)
- let row = tableRowByText(page, data.username)
- await row.getByRole('button', { name: '停用', exact: true }).click()
- await confirmMessageBox(page, '确认停用')
- await expect(tableRowByText(page, data.username)).toContainText('已停用')
- expect((await apiUser(request, token))?.enabled).toBe(false)
- await page.reload()
- await search(page, '请输入姓名、账号、部门或角色', data.username)
- await expect(tableRowByText(page, data.username)).toContainText('已停用')
-
- const user = await apiUser(request, token)
- await logout(page)
- const rejected = await request.post('/api/auth/v1/auth/login', {
- data: { roleCode: 'student', userId: String(user?.id), password: data.initialPassword, rememberMe: false },
- })
- // 公开登录接口故意不泄露“账号已停用”,与账号/密码/身份不匹配统一返回 401。
- expect(rejected.status()).toBe(401)
- await page.locator('[data-role-code="student"]').click()
- const picker = page.locator('.login-identity-picker')
- await expect(picker).toBeVisible()
- await picker.click()
- await expect(page.locator('.login-identity-popper .el-select-dropdown__item').filter({ hasText: data.editedDisplayName })).toHaveCount(0)
- await captureScreenshot(page, testInfo, '10-disabled-user-login-rejected-and-hidden')
-
- await loginAsAdmin(page)
- await page.goto('/organization/users')
- await search(page, '请输入姓名、账号、部门或角色', data.username)
- row = tableRowByText(page, data.username)
- await row.getByRole('button', { name: '启用', exact: true }).click()
- await confirmMessageBox(page, '确认启用')
- await expect(tableRowByText(page, data.username)).toContainText('正常')
- expect((await apiUser(request, token))?.enabled).toBe(true)
- 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 captureScreenshot(page, testInfo, '11-reenabled-user-page-login-restored')
- await clearBrowserSession(page)
- await loginAsAdmin(page)
- })
-
- await test.step('用户页面确认删除后 API 无记录,角色解除关联后通过页面最终删除', async () => {
- await page.goto('/organization/users')
- await search(page, '请输入姓名、账号、部门或角色', data.username)
- let row = tableRowByText(page, data.username)
- await row.getByRole('button', { name: '删除', exact: true }).click()
- await expect(page.locator('.el-message-box:visible')).toContainText(data.username)
- await confirmMessageBox(page, '确认删除')
- await expect(tableRowByText(page, data.username)).toHaveCount(0)
- expect(await apiUser(request, token)).toBeUndefined()
- await captureScreenshot(page, testInfo, '12-user-page-delete-confirmed')
-
- await page.goto('/organization/roles')
- await search(page, '请输入角色名称、编码或说明', data.roleCode)
- row = tableRowByText(page, data.roleCode)
- await expect(row.getByRole('button', { name: '删除', exact: true })).toBeEnabled()
- await row.getByRole('button', { name: '删除', exact: true }).click()
- await confirmMessageBox(page, '删除角色')
- await expect(tableRowByText(page, data.roleCode)).toHaveCount(0)
- expect(await apiRole(request, token)).toBeUndefined()
- await captureScreenshot(page, testInfo, '13-unlinked-role-page-delete-confirmed')
- })
- } catch (error) {
- primaryError = error
- } finally {
- const cleanup = await cleanupCreatedData(request, token).catch((error) => ({ cleanupError: String(error) }))
- await attachJson(testInfo, 'cleanup-result', cleanup)
- }
-
- if (primaryError) throw primaryError
- expect(await apiUser(request, token)).toBeUndefined()
- expect(await apiRole(request, token)).toBeUndefined()
- expect(await apiDepartment(request, token, data.grandchildCode)).toBeUndefined()
- expect(await apiDepartment(request, token, data.childCode)).toBeUndefined()
- expect(await apiDepartment(request, token, data.parentCode)).toBeUndefined()
- })
|