import { createRequire } from 'node:module' import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAuthStore } from '@/core/auth/auth' import { useAccessPreviewStore } from '@/core/access/accessPreview' import AppDialog from '@/shared/components/AppDialog.vue' import InternalUsersView from './InternalUsersView.vue' import { ApiResponseError } from '@/shared/api/http' import * as api from '../api/platformIam' import { listAllPlatformWarehouses } from '@/modules/warehouses' const { default: ElementPlus, ElDialog, ElSelect, ElCheckbox, ElMessageBox } = createRequire(import.meta.url)('element-plus') as typeof import('element-plus') vi.mock('../api/platformIam', () => ({ listPlatformManagedUsers: vi.fn(), getPlatformUserOptions: vi.fn(), listSharedReceivingOverseasWarehouseOptions: vi.fn(), createPlatformManagedUser: vi.fn(), updatePlatformManagedUser: vi.fn(), changePlatformManagedUserStatus: vi.fn(), resetPlatformManagedUserPassword: vi.fn() })) vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() })) const memberId = '9223372036854775807', warehouseId = '9223372036854775806' let record: api.PlatformManagedUser const table = { props: ['data'], template: '
' } function button(label: string, selector = 'button') { const found = Array.from(document.querySelectorAll(selector)).find(node => node.textContent?.trim() === label) expect(found, label).toBeDefined(); return found! } async function view() { const wrapper = mount(InternalUsersView, { attachTo: document.body, global: { plugins: [ElementPlus], stubs: { DataTable: table, QueryToolbar: true } } }); await flushPromises(); return wrapper } async function editor(create = false) { const wrapper = await view(); button(create ? '新增用户' : '编辑').click(); await flushPromises(); return wrapper } async function text(placeholder: string, value: string) { const input = document.querySelector(`.el-dialog [placeholder="${placeholder}"]`)! input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); await flushPromises() } function select(wrapper: Awaited>, placeholder: string) { const found = wrapper.findAllComponents(ElSelect).find(node => node.props('placeholder') === placeholder) expect(found, placeholder).toBeDefined(); return found! } async function fillCreate(wrapper: Awaited>, roles = ['ROLE-OP']) { await text('登录用户名', 'new_operator'); await text('真实姓名', '新操作员'); await text('手机号码', '13800000000'); await text('邮箱地址', 'worker@example.test') select(wrapper, '选择角色').vm.$emit('update:modelValue', roles) await text('8-64 位,包含字母和数字', 'TestPassword123'); await text('再次输入密码', 'TestPassword123'); await flushPromises() } function permissions(values: string[]) { useAuthStore().permissions = ['platform:user:list', ...values]; useAccessPreviewStore().initializeAfterLogin() } enableAutoUnmount(afterEach) afterEach(() => ElMessageBox.close()) beforeEach(() => { vi.resetAllMocks(); document.body.innerHTML = ''; setActivePinia(createPinia()) useAuthStore().token = 'test-session' permissions(['platform:user:create','platform:user:edit','platform:user:status','platform:user:reset-password','platform:user:grant-role']) record = { membershipId: memberId, userId: '9223372036854775805', authUserId: '9223372036854775804', username: 'test_operator', displayName: '测试操作员', phone: '13800000000', email: 'worker@example.test', tenantId: '0', tenantName: '平台', memberType: 'INTERNAL', memberNo: 'EMP-TEST', status: 'ACTIVE', roleIds: ['ROLE-OP'], roleNames: ['运营角色'], lastLoginAt: '2026-10-20T10:30:00', protectedAccount: false, overseasWarehouseId: null } vi.mocked(api.listPlatformManagedUsers).mockImplementation(async () => ({ records: [record], total: 1, page: 1, size: 10, pages: 1 })) vi.mocked(api.getPlatformUserOptions).mockResolvedValue({ tenants: [{ id: '0', name: '平台', status: 'ACTIVE' }], roles: [ { id: 'ROLE-OP', tenantId: '0', roleCode: 'PLATFORM_OPERATOR', name: '运营角色', status: 'ACTIVE' }, { id: 'ROLE-OVER', tenantId: '0', roleCode: 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', name: '海外PDA角色', status: 'ACTIVE' }, { id: 'ROLE-DOM', tenantId: '0', roleCode: 'CN_FIRSTLEG_PDA_OPERATOR', name: '国内PDA角色', status: 'ACTIVE' }, ] }) vi.mocked(api.listSharedReceivingOverseasWarehouseOptions).mockResolvedValue([{ id: warehouseId, warehouseCode: 'US-TEST', warehouseName: '海外收货仓', countryCode: 'US' }]) vi.mocked(listAllPlatformWarehouses).mockResolvedValue([{ id: warehouseId, tenantId: '0', warehouseCode: 'CN-TEST', internalCode: null, warehouseName: '国内作业仓', warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null }]) vi.mocked(api.createPlatformManagedUser).mockImplementation(async () => record) vi.mocked(api.updatePlatformManagedUser).mockImplementation(async () => record) vi.mocked(api.changePlatformManagedUserStatus).mockImplementation(async (_id,status) => ({ ...record, status })) vi.mocked(api.resetPlatformManagedUserPassword).mockResolvedValue(undefined) }) describe('internal user unified dialogs', () => { it('creates a platform account and preserves draft through native fullscreen', async () => { const wrapper = await editor(true); await fillCreate(wrapper) const dialog = wrapper.findComponent(AppDialog) expect(dialog.props()).toMatchObject({ width: 'min(650px, calc(100vw - 28px))', fullscreenable: true, title: '新增用户' }) document.querySelector('[aria-label="展开全屏"]')!.click(); await flushPromises() expect(dialog.findComponent(ElDialog).props('fullscreen')).toBe(true) expect(document.querySelector('[placeholder="登录用户名"]')!.value).toBe('new_operator') button('确认添加', '.el-dialog__footer button').click(); await flushPromises() expect(api.createPlatformManagedUser).toHaveBeenCalledWith({ username: 'new_operator', displayName: '新操作员', phone: '13800000000', email: 'worker@example.test', tenantId: '0', roleIds: ['ROLE-OP'], overseasWarehouseId: null, password: 'TestPassword123', status: 'ACTIVE' }) }) it('enforces required fields and matching initial passwords before creating', async () => { const wrapper = await editor(true) button('确认添加', '.el-dialog__footer button').click(); await flushPromises() expect(api.createPlatformManagedUser).not.toHaveBeenCalled() await vi.waitFor(() => expect(document.querySelector('.el-dialog')?.textContent).toContain('请输入用户名')) await fillCreate(wrapper); await text('再次输入密码', 'Mismatch123') button('确认添加', '.el-dialog__footer button').click(); await flushPromises() expect(api.createPlatformManagedUser).not.toHaveBeenCalled() await vi.waitFor(() => expect(document.querySelector('.el-dialog')?.textContent).toContain('两次输入的密码不一致')) }) it('edits the same membership with readonly login name and timestamp', async () => { const wrapper = await editor() expect(wrapper.findComponent(AppDialog).props('title')).toBe('编辑用户 · 测试操作员') expect(document.querySelector('.el-dialog [placeholder="登录用户名"]')).toBeNull() expect(document.querySelector('.el-dialog')?.textContent).toContain('test_operator') expect(document.querySelector('.el-dialog')?.textContent).toContain('2026-10-20 10:30') expect(document.querySelector('.el-dialog')?.textContent).not.toContain(memberId) await text('真实姓名', '更名操作员') button('保存修改', '.el-dialog__footer button').click(); await flushPromises() expect(api.updatePlatformManagedUser).toHaveBeenCalledWith(memberId, { displayName: '更名操作员', phone: record.phone, email: record.email, tenantId: '0', roleIds: ['ROLE-OP'], overseasWarehouseId: null }) expect(api.resetPlatformManagedUserPassword).not.toHaveBeenCalled() }) it.each(['ROLE-OVER','ROLE-DOM'] as const)('requires and submits the original %s long warehouse binding', async role => { const wrapper = await editor(true); await fillCreate(wrapper, [role]) button('确认添加', '.el-dialog__footer button').click(); await flushPromises() expect(api.createPlatformManagedUser).not.toHaveBeenCalled() const placeholder = role === 'ROLE-OVER' ? '请选择平台共享海外收货仓' : '请选择国内作业仓' select(wrapper, placeholder).vm.$emit('update:modelValue', warehouseId); await flushPromises() button('确认添加', '.el-dialog__footer button').click(); await flushPromises() expect(api.createPlatformManagedUser).toHaveBeenCalledWith(expect.objectContaining(role === 'ROLE-OVER' ? { overseasWarehouseId: warehouseId } : { overseasWarehouseId: null, domesticWarehouseId: warehouseId })) if (role === 'ROLE-DOM') expect(listAllPlatformWarehouses).toHaveBeenCalledWith({ warehouseType: 'DOMESTIC', status: 'ACTIVE' }) }) it('recovers a shared warehouse load failure without discarding the user draft', async () => { vi.mocked(api.listSharedReceivingOverseasWarehouseOptions).mockRejectedValueOnce(new ApiResponseError('目录暂不可用', 'A0500', 500, null, 'req-pda-options')) const wrapper = await editor(true); await fillCreate(wrapper, ['ROLE-OVER']) expect(document.querySelector('.el-dialog .load-error-state')?.textContent).toContain('目录暂不可用') expect(document.querySelector('.el-dialog .load-error-state')?.textContent).toContain('req-pda-options') button('重新加载', '.el-dialog button').click(); await flushPromises() expect(api.listSharedReceivingOverseasWarehouseOptions).toHaveBeenCalledTimes(2) expect(document.querySelector('.el-dialog .load-error-state')).toBeNull() expect(document.querySelector('[placeholder="登录用户名"]')!.value).toBe('new_operator') }) it('keeps protected role and status as readonly facts and disables the row status action', async () => { record.protectedAccount = true const wrapper = await editor() expect(button('禁用', '.real-row button').disabled).toBe(true) expect(wrapper.findAllComponents(ElSelect)).toHaveLength(0) expect(document.querySelector('[aria-label="用户基本资料"]')?.textContent).toContain('运营角色') expect(document.querySelector('[aria-label="用户基本资料"]')?.textContent).toContain('正常') expect(api.changePlatformManagedUserStatus).not.toHaveBeenCalled() }) it('allows only the original status command when editing with status permission alone', async () => { permissions(['platform:user:status']) const wrapper = await editor() expect(document.querySelector('.el-dialog [placeholder="真实姓名"]')).toBeNull() expect(document.querySelector('.el-dialog [placeholder="手机号码"]')).toBeNull() expect(document.querySelector('[aria-label="用户登录密码"]')).toBeNull() wrapper.findComponent(ElSelect).vm.$emit('update:modelValue', 'DISABLED'); await flushPromises() button('保存修改', '.el-dialog__footer button').click(); await flushPromises() expect(api.changePlatformManagedUserStatus).toHaveBeenCalledWith(memberId, 'DISABLED') expect(api.updatePlatformManagedUser).not.toHaveBeenCalled(); expect(api.resetPlatformManagedUserPassword).not.toHaveBeenCalled() }) it('uses the separate reset command without changing profile for reset-only permission', async () => { permissions(['platform:user:reset-password']) const wrapper = await editor() expect(document.querySelector('.el-dialog [placeholder="真实姓名"]')).toBeNull() await wrapper.findComponent(ElCheckbox).get('input').setValue(true); await flushPromises() await text('8-64 位,包含字母和数字', 'ResetPassword123'); await text('再次输入密码', 'ResetPassword123') button('保存修改', '.el-dialog__footer button').click(); await flushPromises() expect(api.resetPlatformManagedUserPassword).toHaveBeenCalledWith(memberId, 'ResetPassword123') expect(api.updatePlatformManagedUser).not.toHaveBeenCalled(); expect(api.changePlatformManagedUserStatus).not.toHaveBeenCalled() }) it('locks all close paths during saving and retains editable values on failure', async () => { let rejectSave!: (error: Error) => void vi.mocked(api.updatePlatformManagedUser).mockImplementation(() => new Promise((_resolve,reject) => { rejectSave = reject })) const wrapper = await editor(); await text('真实姓名', '失败草稿') button('保存修改', '.el-dialog__footer button').click(); await flushPromises() expect(wrapper.findComponent(ElDialog).props()).toMatchObject({ showClose: false, closeOnPressEscape: false, closeOnClickModal: false }) expect(button('取消', '.el-dialog__footer button').disabled).toBe(true) expect(document.querySelector('[placeholder="真实姓名"]')!.disabled).toBe(true) rejectSave(new Error('更新失败')); await flushPromises() expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true) expect(document.querySelector('[placeholder="真实姓名"]')!.value).toBe('失败草稿') }) it.each(['ACTIVE','DISABLED'] as const)('identifies %s status confirmation without exposing account IDs', async status => { record.status = status const verb = status === 'ACTIVE' ? '禁用' : '启用' await view(); button(verb, '.real-row button').click(); await flushPromises() expect(document.querySelector('.el-message-box__title')?.textContent).toBe(`${verb}用户 · 测试操作员`) expect(document.querySelector('.el-message-box')?.textContent).not.toContain(memberId) expect(api.changePlatformManagedUserStatus).not.toHaveBeenCalled() button(`确认${verb}`, '.el-message-box button').click(); await flushPromises() expect(api.changePlatformManagedUserStatus).toHaveBeenCalledWith(memberId, status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE') }) it('keeps creation hidden without grant-role permission', async () => { permissions(['platform:user:create']) const wrapper = await view() expect(wrapper.findAll('button').some(node => node.text() === '新增用户')).toBe(false) expect(api.createPlatformManagedUser).not.toHaveBeenCalled() }) })