891 lines
48 KiB
TypeScript
891 lines
48 KiB
TypeScript
import { ElMessageBox } from 'element-plus'
|
|
import { createRequire } from 'node:module'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
approveEnterpriseReview,
|
|
archivePlatformTenant,
|
|
createPlatformTenant,
|
|
downloadTenantQualification,
|
|
createPlatformTenantImpersonation,
|
|
getPlatformTenant,
|
|
listEnterpriseReviews,
|
|
listPlatformTenants,
|
|
listTenantQualifications,
|
|
rejectEnterpriseReview,
|
|
resetPlatformTenantOwnerPassword,
|
|
submitPlatformTenantReview,
|
|
updatePlatformTenant,
|
|
uploadPlatformTenantQualification,
|
|
uploadPlatformTenantRequiredQualificationPackage,
|
|
type EnterpriseReview,
|
|
type PlatformTenantResource,
|
|
} from '@/modules/iam'
|
|
import { createPlatformExportTask } from '@/modules/downloads'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
|
import CustomersView from './CustomersView.vue'
|
|
|
|
const { default: ElementPlus } = createRequire(import.meta.url)('element-plus') as typeof import('element-plus')
|
|
|
|
const customersSource = readFileSync(resolve(process.cwd(), 'src/modules/customers/pages/CustomersView.vue'), 'utf8')
|
|
|
|
const { routerPush, routerReplace, routeControl } = vi.hoisted(() => ({
|
|
routerPush: vi.fn(),
|
|
routerReplace: vi.fn(),
|
|
routeControl: { setEditId: (_editId: string) => {} },
|
|
}))
|
|
|
|
vi.mock('vue-router', async (importOriginal) => ({
|
|
...await importOriginal<typeof import('vue-router')>(),
|
|
...await (async () => {
|
|
const { reactive } = await import('vue')
|
|
const route = reactive<{ query: { edit?: string } }>({ query: {} })
|
|
routeControl.setEditId = (editId: string) => {
|
|
if (editId) route.query.edit = editId
|
|
else delete route.query.edit
|
|
}
|
|
return { useRoute: () => route }
|
|
})(),
|
|
useRouter: () => ({ push: routerPush, replace: routerReplace }),
|
|
}))
|
|
|
|
vi.mock('@/modules/iam', () => ({
|
|
approveEnterpriseReview: vi.fn(),
|
|
archivePlatformTenant: vi.fn(),
|
|
changePlatformTenantStatus: vi.fn(),
|
|
checkPlatformTenantContactPhone: vi.fn().mockResolvedValue(true),
|
|
createPlatformTenant: vi.fn(),
|
|
createPlatformTenantImpersonation: vi.fn(),
|
|
downloadTenantQualification: vi.fn(),
|
|
getPlatformTenant: vi.fn(),
|
|
listEnterpriseReviews: vi.fn(),
|
|
listPlatformTenantOrderCounts: vi.fn(),
|
|
listPlatformTenants: vi.fn(),
|
|
listTenantQualifications: vi.fn(),
|
|
qualificationFileName: vi.fn((documentUrl: string) => documentUrl.split('/').at(-1) || '资质文件'),
|
|
rejectEnterpriseReview: vi.fn(),
|
|
resetPlatformTenantOwnerPassword: vi.fn(),
|
|
submitPlatformTenantReview: vi.fn(),
|
|
updatePlatformTenant: vi.fn(),
|
|
uploadPlatformTenantQualification: vi.fn(),
|
|
uploadPlatformTenantRequiredQualificationPackage: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
|
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
|
|
|
|
class ResizeObserverStub implements ResizeObserver {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
}
|
|
|
|
const fullTenant: PlatformTenantResource = {
|
|
id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', customerNickname: '柏', status: 'PROFILE_REQUIRED',
|
|
legalName: '柏悦物流有限公司', legalRepresentative: '张三', unifiedSocialCreditCode: '91440300MA5ABCDE12',
|
|
contactName: '张三', contactPhone: '13800138000', contactEmail: 'ops@example.com',
|
|
servicePackage: '基础版', contactWechat: 'baiyue-ops', salesOwnerName: '李四', paymentTermDays: 30,
|
|
enterpriseType: '有限责任公司', establishedDate: '2020-01-01', registeredCapital: 1000,
|
|
registeredCapitalCurrency: 'CNY', registeredAddress: '深圳市宝安区', internalRemark: '重点客户',
|
|
createdAt: '2026-08-01T10:00:00', approvedAt: null,
|
|
}
|
|
|
|
const pendingReview: EnterpriseReview = {
|
|
id: '190000000000000001',
|
|
tenantId: fullTenant.id,
|
|
tenantName: fullTenant.tenantName,
|
|
tenantCode: fullTenant.tenantCode,
|
|
status: 'PENDING',
|
|
}
|
|
|
|
function tableButtonByText(text: string) {
|
|
return [...document.querySelectorAll<HTMLButtonElement>('.el-table__body button')]
|
|
.find((button) => button.textContent?.trim() === text)
|
|
}
|
|
|
|
function selectQualificationFile(index: number, file: File) {
|
|
const input = document.querySelectorAll<HTMLInputElement>('.qualification-file input[type="file"]')[index]
|
|
Object.defineProperty(input, 'files', { configurable: true, value: [file] })
|
|
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
}
|
|
|
|
async function clickMoreAction(text: string) {
|
|
document.querySelector<HTMLButtonElement>('.el-table__body button[aria-label^="更多客户操作:"]')?.click()
|
|
await flushPromises()
|
|
const item = [...document.querySelectorAll<HTMLElement>('.el-dropdown-menu__item')]
|
|
.find((candidate) => candidate.textContent?.trim() === text && !candidate.classList.contains('is-disabled'))
|
|
expect(item).toBeDefined()
|
|
item!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
await flushPromises()
|
|
}
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
describe('CustomersView', () => {
|
|
let pinia: ReturnType<typeof createPinia>
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
routeControl.setEditId('')
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
|
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:qualification-preview') })
|
|
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
|
|
document.body.innerHTML = ''
|
|
pinia = createPinia()
|
|
setActivePinia(pinia)
|
|
const auth = useAuthStore()
|
|
auth.token = 'token'
|
|
auth.permissions = ['platform:customer:list', 'platform:customer:edit', 'platform:customer:export', 'platform:download:list']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({
|
|
records: [{ ...fullTenant, contactPhone: '138****8000', contactEmail: 'o***@example.com' }],
|
|
page: 1, size: 10, total: 1, pages: 1,
|
|
})
|
|
vi.mocked(getPlatformTenant).mockResolvedValue(fullTenant)
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([])
|
|
vi.mocked(listEnterpriseReviews).mockResolvedValue([])
|
|
vi.mocked(createPlatformTenant).mockResolvedValue({
|
|
tenant: fullTenant, administratorUsername: 'merchant_owner', temporaryPassword: 'Temporary#2026',
|
|
})
|
|
vi.mocked(submitPlatformTenantReview).mockResolvedValue({ ...fullTenant, status: 'PENDING' })
|
|
vi.mocked(updatePlatformTenant).mockResolvedValue(fullTenant)
|
|
vi.mocked(uploadPlatformTenantQualification).mockResolvedValue({
|
|
qualificationId: 'qualification-1', reviewId: null, tenantStatus: 'PROFILE_REQUIRED', documentUrl: '/files/license.png',
|
|
})
|
|
vi.mocked(uploadPlatformTenantRequiredQualificationPackage).mockResolvedValue({
|
|
businessLicenseQualificationId: 'qualification-business',
|
|
legalPersonIdQualificationId: 'qualification-legal',
|
|
tenantStatus: 'PROFILE_REQUIRED',
|
|
businessLicenseDocumentUrl: '/files/license.png',
|
|
legalPersonIdDocumentUrl: '/files/legal-person-new.png',
|
|
})
|
|
vi.mocked(createPlatformExportTask).mockResolvedValue({
|
|
id: 'export-1', taskName: '客户列表', taskNo: 'EXP2084', status: 'PENDING',
|
|
} as never)
|
|
vi.mocked(showExportTaskCreatedDialog).mockResolvedValue(false)
|
|
})
|
|
|
|
it('waits for the client workspace to confirm impersonation session persistence', () => {
|
|
expect(customersSource).toContain("searchParams.set('impersonationOrigin', window.location.origin)")
|
|
expect(customersSource).toContain("event.data?.type !== 'impersonation-complete'")
|
|
expect(customersSource).toContain("event.data?.type === 'impersonation-failed'")
|
|
expect(customersSource).toContain("ElMessage.success('客户工作台登录成功')")
|
|
})
|
|
|
|
it('searches the customer list by customer name only', () => {
|
|
expect(customersSource).toContain("label: '客户名称', placeholder: '请输入客户名称'")
|
|
expect(customersSource).toContain("label: '客户昵称'")
|
|
expect(customersSource).toContain('中文拼音大写首字母,如 SZWL')
|
|
expect(customersSource).not.toContain('公司名称、客户编号或联系人')
|
|
expect(customersSource).not.toContain('row.tenantCode.toLowerCase().includes(keyword)')
|
|
expect(customersSource).not.toContain('String(row.id).includes(keyword)')
|
|
})
|
|
|
|
it('matches the frozen prototype containers for customer operations', () => {
|
|
expect(customersSource).toContain("router.push({ name: 'customer-detail'")
|
|
expect(customersSource).toContain("const editorDialogWidth = 'min(1120px, calc(100vw - 28px))'")
|
|
expect(customersSource.match(/class="density-table density-table--editable customer-editor-table"/g)).toHaveLength(2)
|
|
expect(customersSource).toContain('width="min(520px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog customer-password-dialog admin-workbench is-dialog"')
|
|
expect(customersSource).toContain('width="min(1040px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog review-decision-dialog admin-workbench is-dialog"')
|
|
expect(customersSource).not.toContain('<AppDrawer v-model="editorOpen"')
|
|
})
|
|
|
|
it('keeps compact customer fields in three columns and leaves wide fields and validation readable', async () => {
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')!.click()
|
|
await flushPromises()
|
|
const tables = Array.from(document.querySelectorAll<HTMLTableElement>('.customer-editor-dialog .customer-editor-table'))
|
|
expect(tables).toHaveLength(2)
|
|
expect(Array.from(tables[0]!.rows, row => row.cells.length)).toEqual([3, 3, 1, 1])
|
|
expect(Array.from(tables[1]!.rows, row => row.cells.length)).toEqual([3, 3, 1])
|
|
expect(tables.flatMap(table => Array.from(table.querySelectorAll('td[colspan]'), cell => cell.getAttribute('colspan')))).toEqual(['3', '3', '3'])
|
|
expect(document.querySelector<HTMLElement>('.customer-editor-dialog .qualification-credit-code .el-form-item__label')?.style.width).toBe('144px')
|
|
const company = document.querySelector<HTMLInputElement>('.customer-editor-dialog input[placeholder="请输入工商登记公司名称"]')!
|
|
expect(company.closest('.el-form-item')?.querySelector<HTMLElement>('.el-form-item__label')?.style.width).toBe('120px')
|
|
company.value = ''
|
|
company.dispatchEvent(new Event('input', { bubbles: true }))
|
|
await flushPromises()
|
|
company.dispatchEvent(new FocusEvent('blur'))
|
|
await vi.waitFor(() => expect(company.closest('.el-form-item')?.querySelector('.el-form-item__error')?.textContent).toContain('请输入公司名称'))
|
|
expect(document.querySelector('.customer-editor-dialog .el-dialog__footer')?.textContent).toContain('保存资料和资质')
|
|
expect(updatePlatformTenant).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('keeps qualification file controls usable when the editor dialog is narrow', () => {
|
|
expect(customersSource).toContain('class="qualification-validity" role="cell"')
|
|
expect(customersSource).toContain('grid-template-columns:170px minmax(0,1fr) 220px')
|
|
expect(customersSource).toContain('.qualification-table { container-type:inline-size;')
|
|
expect(customersSource).toContain('@container (max-width:660px)')
|
|
expect(customersSource).toContain('.qualification-row { grid-template-columns:112px minmax(0,1fr); }')
|
|
expect(customersSource).toContain('.qualification-label { grid-row:span 2; align-items:center; }')
|
|
expect(customersSource).toContain('.qualification-validity :deep(.el-date-editor) { width:100%; min-width:0; }')
|
|
expect(customersSource).toContain('.qualification-file-name { min-width:0;')
|
|
expect(readFileSync(resolve(process.cwd(), 'src/modules/customers/customerDensity.css'), 'utf8')).toContain('max-width: none; overflow-wrap: anywhere; white-space: normal;')
|
|
})
|
|
|
|
it('loads the complete customer detail before editing masked list fields', async () => {
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
|
|
expect(getPlatformTenant).toHaveBeenCalledWith(fullTenant.id)
|
|
const phone = document.querySelector<HTMLInputElement>('input[placeholder="请输入公司联系电话"]')
|
|
const email = document.querySelector<HTMLInputElement>('input[placeholder="如:contact@example.com"]')
|
|
expect(phone?.value).toBe(fullTenant.contactPhone)
|
|
expect(email?.value).toBe(fullTenant.contactEmail)
|
|
expect(document.querySelector('.customer-editor-dialog .el-dialog__title')?.textContent).toBe('编辑客户 · TEN8')
|
|
expect(document.querySelector('input[placeholder="请输入18位大写字母或数字"]')).toBeNull()
|
|
expect(document.querySelector('.qualification-credit-code')?.textContent).toContain(fullTenant.unifiedSocialCreditCode)
|
|
phone!.value = '13900139000'
|
|
phone!.dispatchEvent(new Event('input', { bubbles: true }))
|
|
document.querySelector<HTMLButtonElement>('.customer-editor-dialog button[aria-label="展开全屏"]')!.click()
|
|
await flushPromises()
|
|
expect(document.querySelector('.customer-editor-dialog.is-fullscreen')).not.toBeNull()
|
|
expect(document.querySelector<HTMLInputElement>('input[placeholder="请输入公司联系电话"]')?.value).toBe('13900139000')
|
|
document.querySelector<HTMLButtonElement>('.customer-editor-dialog button[aria-label="退出全屏"]')!.click()
|
|
await flushPromises()
|
|
expect(document.querySelector<HTMLInputElement>('input[placeholder="请输入公司联系电话"]')?.value).toBe('13900139000')
|
|
})
|
|
|
|
it('shows an authenticated image preview for an uploaded qualification', async () => {
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([{
|
|
id: 'qualification-1', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
|
|
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png',
|
|
status: 'PENDING', reviewComment: null, validTo: null,
|
|
}])
|
|
vi.mocked(downloadTenantQualification).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
await flushPromises()
|
|
|
|
expect(downloadTenantQualification).toHaveBeenCalledWith(fullTenant.id, '/files/license.png')
|
|
const preview = document.querySelector<HTMLImageElement>('img[alt="营业执照预览"]')
|
|
expect(preview?.src).toBe('blob:qualification-preview')
|
|
})
|
|
|
|
it('marks only the business license as a required file when creating a customer', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:create']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '新增客户')?.click()
|
|
await flushPromises()
|
|
|
|
const labels = [...document.querySelectorAll<HTMLElement>('.qualification-label')]
|
|
expect(labels.map((label) => label.textContent?.replace(/\s/g, ''))).toEqual([
|
|
'*营业执照',
|
|
'法人身份证选填',
|
|
'货代资质证书选填',
|
|
'银行开户许可证选填',
|
|
])
|
|
expect(document.querySelector('.qualification-section')?.textContent).not.toContain('营业执照和18位统一社会信用代码为必填业务项')
|
|
document.querySelector<HTMLButtonElement>('[aria-label="查看资质提交规则"]')!.focus()
|
|
await vi.waitFor(() => expect(document.body.textContent).toContain('营业执照和18位统一社会信用代码为必填业务项'))
|
|
})
|
|
|
|
it('creates a customer with a business license and no legal-person ID', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:create']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '新增客户')?.click()
|
|
await flushPromises()
|
|
|
|
await wrapper.get('input[placeholder="请输入工商登记公司名称"]').setValue(fullTenant.tenantName)
|
|
await wrapper.get('input[placeholder="请输入营业执照上的企业名称"]').setValue(fullTenant.legalName)
|
|
expect(wrapper.get('input[placeholder="请输入法定代表人姓名(选填)"]').attributes('aria-required')).not.toBe('true')
|
|
await wrapper.get('input[placeholder="请输入主要联系人姓名"]').setValue(fullTenant.contactName)
|
|
await wrapper.get('input[placeholder="请输入公司联系电话"]').setValue(fullTenant.contactPhone)
|
|
await wrapper.get('input[placeholder="请输入18位大写字母或数字"]').setValue(fullTenant.unifiedSocialCreditCode)
|
|
const licenseFile = new File(['license'], 'license.png', { type: 'image/png' })
|
|
selectQualificationFile(0, licenseFile)
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.includes('保存资料和资质'))?.click()
|
|
await flushPromises()
|
|
|
|
expect(createPlatformTenant).toHaveBeenCalledWith(expect.objectContaining({
|
|
tenantName: fullTenant.tenantName,
|
|
contactPhone: fullTenant.contactPhone,
|
|
legalRepresentative: '',
|
|
}))
|
|
expect(createPlatformTenant).toHaveBeenCalledWith(expect.not.objectContaining({ unifiedSocialCreditCode: expect.anything() }))
|
|
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledWith(
|
|
fullTenant.id, licenseFile, null, fullTenant.unifiedSocialCreditCode, false, false,
|
|
)
|
|
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false)
|
|
})
|
|
|
|
it('keeps the company contact phone editable without changing a member login phone', async () => {
|
|
const activeTenant = { ...fullTenant, status: 'ACTIVE' as const }
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [activeTenant], page: 1, size: 10, total: 1, pages: 1 })
|
|
vi.mocked(getPlatformTenant).mockResolvedValue(activeTenant)
|
|
vi.mocked(updatePlatformTenant).mockResolvedValue(activeTenant)
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([{
|
|
id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
|
|
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'APPROVED', reviewComment: null, validTo: null,
|
|
}])
|
|
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
|
|
await wrapper.get('[aria-label="查看企业联系信息说明"]').trigger('focus')
|
|
await vi.waitFor(() => expect(document.body.textContent).toContain('不会修改企业管理员登录手机号'))
|
|
const phone = wrapper.get('input[placeholder="请输入公司联系电话"]')
|
|
expect(phone.attributes('disabled')).toBeUndefined()
|
|
await phone.setValue('0755-12345678')
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.includes('保存资料和资质'))?.click()
|
|
await flushPromises()
|
|
|
|
expect(updatePlatformTenant).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ contactPhone: '0755-12345678' }))
|
|
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false)
|
|
expect(document.body.textContent).toContain('客户资料已保存')
|
|
expect(document.body.textContent).not.toContain('尚未提交审核')
|
|
expect(customersSource).not.toContain('联系电话(登录用户名)')
|
|
expect(customersSource.indexOf('if (!failures.length) editorOpen.value = false')).toBeLessThan(customersSource.indexOf('await loadCustomers()'))
|
|
})
|
|
|
|
it('allows an active customer to replace qualifications and select a validity date', async () => {
|
|
const activeTenant = { ...fullTenant, status: 'ACTIVE' as const }
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [activeTenant], page: 1, size: 10, total: 1, pages: 1 })
|
|
vi.mocked(getPlatformTenant).mockResolvedValue(activeTenant)
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([
|
|
{
|
|
id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
|
|
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'APPROVED', reviewComment: null, validTo: null,
|
|
},
|
|
{
|
|
id: 'qualification-freight', tenantId: fullTenant.id, qualificationType: 'FREIGHT_FORWARDER_CERTIFICATE',
|
|
certificateNo: null, documentUrl: '/files/freight.png', status: 'APPROVED', reviewComment: null, validTo: null,
|
|
},
|
|
])
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
|
|
const fileInputs = [...document.querySelectorAll<HTMLInputElement>('.qualification-file input[type="file"]')]
|
|
const validityInputs = [...document.querySelectorAll<HTMLInputElement>('.qualification-validity input')]
|
|
expect(fileInputs).toHaveLength(4)
|
|
expect(fileInputs.every((input) => !input.disabled)).toBe(true)
|
|
expect(validityInputs.map((input) => input.disabled)).toEqual([true, false, true, false])
|
|
|
|
selectQualificationFile(0, new File(['license-new'], 'license-new.png', { type: 'image/png' }))
|
|
await flushPromises()
|
|
expect(validityInputs[0]?.disabled).toBe(false)
|
|
})
|
|
|
|
it('does not count rejected or superseded history as a current qualification when resubmitting', async () => {
|
|
const rejectedTenant = { ...fullTenant, status: 'REJECTED' }
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [rejectedTenant], page: 1, size: 10, total: 1, pages: 1 })
|
|
vi.mocked(getPlatformTenant).mockResolvedValue(rejectedTenant)
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([
|
|
{
|
|
id: 'qualification-old', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
|
|
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/old-license.png',
|
|
status: 'SUPERSEDED', reviewComment: '已替换', validTo: null,
|
|
},
|
|
{
|
|
id: 'qualification-rejected', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
|
|
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/rejected-license.png',
|
|
status: 'REJECTED', reviewComment: '请重新上传', validTo: null,
|
|
},
|
|
])
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
|
|
expect(document.querySelector('.qualification-label [aria-label="已上传"]')).toBeNull()
|
|
expect(document.querySelector('.qualification-file-name')?.textContent).toContain('未选择文件')
|
|
expect(downloadTenantQualification).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('opens customer detail as an independent route', async () => {
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
tableButtonByText('详情')?.click()
|
|
|
|
expect(routerPush).toHaveBeenCalledWith({ name: 'customer-detail', params: { tenantId: String(fullTenant.id) } })
|
|
expect(getPlatformTenant).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('opens the requested editor when a cached list receives an edit query', async () => {
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
routeControl.setEditId(String(fullTenant.id))
|
|
await flushPromises()
|
|
|
|
expect(getPlatformTenant).toHaveBeenCalledWith(fullTenant.id)
|
|
expect(document.querySelector<HTMLInputElement>('input[placeholder="请输入公司联系电话"]')?.value).toBe(fullTenant.contactPhone)
|
|
expect(routerReplace).toHaveBeenCalledWith({ name: 'customers' })
|
|
})
|
|
|
|
it('saves the business license, credit code and optional legal-person ID through one package action', async () => {
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([{
|
|
id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID',
|
|
certificateNo: null, documentUrl: '/files/legal-person.png', status: 'PENDING', reviewComment: null, validTo: null,
|
|
}])
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
|
|
const licenseFile = new File(['license'], 'license.png', { type: 'image/png' })
|
|
const legalPersonFile = new File(['legal-person'], 'legal-person.png', { type: 'image/png' })
|
|
selectQualificationFile(0, licenseFile)
|
|
selectQualificationFile(1, legalPersonFile)
|
|
await flushPromises()
|
|
|
|
const buttons = [...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
expect(buttons.some((button) => button.textContent?.trim() === '上传已选资质')).toBe(false)
|
|
buttons.find((button) => button.textContent?.trim() === '保存资料和资质')?.click()
|
|
await flushPromises()
|
|
|
|
expect(updatePlatformTenant).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ tenantName: fullTenant.tenantName }))
|
|
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledWith(
|
|
fullTenant.id,
|
|
licenseFile,
|
|
legalPersonFile,
|
|
fullTenant.unifiedSocialCreditCode,
|
|
false,
|
|
true,
|
|
)
|
|
expect(uploadPlatformTenantQualification).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('replaces only the business license while preserving an existing legal-person ID', async () => {
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([
|
|
{ id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE', certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'PENDING', reviewComment: null, validTo: null },
|
|
{ id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID', certificateNo: null, documentUrl: '/files/legal-person.png', status: 'PENDING', reviewComment: null, validTo: null },
|
|
])
|
|
vi.mocked(uploadPlatformTenantRequiredQualificationPackage).mockResolvedValueOnce({
|
|
businessLicenseQualificationId: 'qualification-business-new', legalPersonIdQualificationId: null,
|
|
tenantStatus: 'PROFILE_REQUIRED', businessLicenseDocumentUrl: '/files/license-new.png', legalPersonIdDocumentUrl: null,
|
|
})
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
|
|
const licenseFile = new File(['license-new'], 'license-new.png', { type: 'image/png' })
|
|
selectQualificationFile(0, licenseFile)
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.includes('保存资料和资质'))?.click()
|
|
await flushPromises()
|
|
|
|
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledWith(
|
|
fullTenant.id, licenseFile, null, fullTenant.unifiedSocialCreditCode, true, false,
|
|
)
|
|
expect(uploadPlatformTenantQualification).not.toHaveBeenCalled()
|
|
expect(document.body.textContent).toContain('法人身份证可按需补充')
|
|
})
|
|
|
|
it('uploads an optional legal-person ID independently when the required business license already exists', async () => {
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([{
|
|
id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
|
|
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'PENDING', reviewComment: null, validTo: null,
|
|
}])
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
|
|
const legalPersonFile = new File(['legal-person'], 'legal-person.png', { type: 'image/png' })
|
|
selectQualificationFile(1, legalPersonFile)
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.includes('保存资料和资质'))?.click()
|
|
await flushPromises()
|
|
|
|
expect(uploadPlatformTenantRequiredQualificationPackage).not.toHaveBeenCalled()
|
|
expect(uploadPlatformTenantQualification).toHaveBeenCalledWith(
|
|
fullTenant.id, 'LEGAL_PERSON_ID', legalPersonFile, undefined, undefined, false,
|
|
)
|
|
})
|
|
|
|
it('retains a selected business license after an upload failure so the operator can retry', async () => {
|
|
vi.mocked(uploadPlatformTenantRequiredQualificationPackage)
|
|
.mockRejectedValueOnce(new Error('upload unavailable'))
|
|
.mockResolvedValueOnce({
|
|
businessLicenseQualificationId: 'qualification-business', legalPersonIdQualificationId: null,
|
|
tenantStatus: 'PROFILE_REQUIRED', businessLicenseDocumentUrl: '/files/license-new.png', legalPersonIdDocumentUrl: null,
|
|
})
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
|
|
const licenseFile = new File(['license-new'], 'retry-license.png', { type: 'image/png' })
|
|
selectQualificationFile(0, licenseFile)
|
|
await flushPromises()
|
|
const saveButton = [...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.includes('保存资料和资质'))
|
|
saveButton?.click()
|
|
await flushPromises()
|
|
expect(document.body.textContent).toContain('retry-license.png')
|
|
|
|
saveButton?.click()
|
|
await flushPromises()
|
|
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('submits a customer for review with a business license and no legal-person ID', async () => {
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([{
|
|
id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
|
|
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'PENDING', reviewComment: null, validTo: null,
|
|
}])
|
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('编辑')?.click()
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '提交审核')?.click()
|
|
await flushPromises()
|
|
|
|
expect(ElMessageBox.confirm).toHaveBeenCalled()
|
|
expect(submitPlatformTenantReview).toHaveBeenCalledWith(fullTenant.id)
|
|
})
|
|
|
|
it('submits the current filters to the backend export service without loading every customer page', async () => {
|
|
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
wrapper.findComponent(QueryToolbar).vm.$emit('update:modelValue', { keyword: ' Acme ', status: 'ACTIVE' })
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const exportButton = [...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '导出Excel')
|
|
exportButton?.click()
|
|
await flushPromises()
|
|
|
|
expect(createPlatformExportTask).toHaveBeenCalledWith({
|
|
exportType: 'CUSTOMER_LIST', format: 'XLSX', taskName: '客户列表',
|
|
parameters: { keyword: 'Acme', status: 'ACTIVE' },
|
|
})
|
|
expect(showExportTaskCreatedDialog).toHaveBeenCalledWith({ taskName: '客户列表', taskNo: 'EXP2084' })
|
|
expect(listPlatformTenants).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('opens the download center when the export-created dialog action is confirmed', async () => {
|
|
vi.mocked(createPlatformExportTask).mockResolvedValue({
|
|
id: 'export-1', taskName: '客户列表', taskNo: 'EXP2084', status: 'PENDING',
|
|
} as never)
|
|
vi.mocked(showExportTaskCreatedDialog).mockResolvedValue(true)
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
const exportButton = [...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '导出Excel')
|
|
exportButton?.click()
|
|
await flushPromises()
|
|
|
|
expect(showExportTaskCreatedDialog).toHaveBeenCalledWith({ taskName: '客户列表', taskNo: 'EXP2084' })
|
|
expect(routerPush).toHaveBeenCalledWith({ name: 'download-center' })
|
|
})
|
|
|
|
it('hides customer export when the stable export permission is absent', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = ['platform:customer:list']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
expect([...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.some((button) => button.textContent?.trim() === '导出Excel')).toBe(false)
|
|
expect(createPlatformExportTask).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not create an unrecoverable export for a role without download-center access', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = ['platform:customer:list', 'platform:customer:export']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
expect([...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.some((button) => button.textContent?.trim() === '导出Excel')).toBe(false)
|
|
expect(createPlatformExportTask).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('approves a pending enterprise directly from the customer list', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:review', 'platform:customer:approve']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({
|
|
records: [{ ...fullTenant, status: 'PENDING' }], page: 1, size: 10, total: 1, pages: 1,
|
|
})
|
|
vi.mocked(listEnterpriseReviews).mockResolvedValue([pendingReview])
|
|
vi.mocked(approveEnterpriseReview).mockResolvedValue({
|
|
tenantId: fullTenant.id,
|
|
administratorCreated: true,
|
|
administratorUsername: 'merchant_8_owner',
|
|
temporaryPassword: 'Temporary#2026',
|
|
})
|
|
|
|
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('审核')?.click()
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '审核通过')?.click()
|
|
await flushPromises()
|
|
|
|
expect(approveEnterpriseReview).toHaveBeenCalledWith(pendingReview.id, '企业资料、营业执照和统一社会信用代码完整、清晰,审核通过(法人身份证为选填项)')
|
|
expect(rejectEnterpriseReview).not.toHaveBeenCalled()
|
|
expect(document.body.textContent).toContain('请立即保存以下一次性凭证')
|
|
expect(document.body.textContent).toContain('merchant_8_owner')
|
|
expect(document.body.textContent).toContain('Temporary#2026')
|
|
const credentials = wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('title').startsWith('审核通过,管理员账号已创建'))!
|
|
expect(credentials.props()).toMatchObject({ modelValue: true, showClose: false, closeOnPressEscape: false, closeOnClickModal: false })
|
|
expect(document.querySelector('.approval-result-dialog .el-dialog__headerbtn')).toBeNull()
|
|
document.querySelector<HTMLButtonElement>('.approval-result-dialog .el-dialog__footer button')!.click()
|
|
await flushPromises()
|
|
expect(credentials.props('modelValue')).toBe(false)
|
|
expect(document.querySelector('.approval-result-dialog code')).toBeNull()
|
|
})
|
|
|
|
it('loads and previews authenticated qualification files in the review dialog', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:review', 'platform:customer:approve']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({
|
|
records: [{ ...fullTenant, status: 'PENDING' }], page: 1, size: 10, total: 1, pages: 1,
|
|
})
|
|
vi.mocked(listEnterpriseReviews).mockResolvedValue([pendingReview])
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([
|
|
{ id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE', certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'PENDING', reviewComment: null, validTo: null },
|
|
{ id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID', certificateNo: null, documentUrl: '/files/legal-person.png', status: 'PENDING', reviewComment: null, validTo: null },
|
|
])
|
|
vi.mocked(downloadTenantQualification).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('审核')?.click()
|
|
await flushPromises()
|
|
|
|
expect(listTenantQualifications).toHaveBeenCalledWith(fullTenant.id)
|
|
expect(document.body.textContent).toContain('营业执照')
|
|
expect(document.body.textContent).toContain('法人身份证')
|
|
const previewButton = [...document.querySelectorAll<HTMLButtonElement>('.review-document-actions button')]
|
|
.find((button) => button.textContent?.trim() === '预览')
|
|
previewButton?.click()
|
|
await flushPromises()
|
|
|
|
expect(downloadTenantQualification).toHaveBeenCalledWith(fullTenant.id, '/files/license.png')
|
|
expect(document.querySelector<HTMLImageElement>('img[alt="license.png预览"]')?.src).toBe('blob:qualification-preview')
|
|
})
|
|
|
|
it('keeps the client registration account as administrator after approval', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:review', 'platform:customer:approve']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({
|
|
records: [{ ...fullTenant, status: 'PENDING' }], page: 1, size: 10, total: 1, pages: 1,
|
|
})
|
|
vi.mocked(listEnterpriseReviews).mockResolvedValue([pendingReview])
|
|
vi.mocked(approveEnterpriseReview).mockResolvedValue({
|
|
tenantId: fullTenant.id,
|
|
administratorCreated: false,
|
|
administratorUsername: null,
|
|
temporaryPassword: null,
|
|
})
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('审核')?.click()
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '审核通过')?.click()
|
|
await flushPromises()
|
|
|
|
expect(approveEnterpriseReview).toHaveBeenCalledWith(pendingReview.id, '企业资料、营业执照和统一社会信用代码完整、清晰,审核通过(法人身份证为选填项)')
|
|
expect(rejectEnterpriseReview).not.toHaveBeenCalled()
|
|
expect(document.body.textContent).toContain('注册账号继续作为企业管理员')
|
|
expect(document.body.textContent).not.toContain('请立即保存以下一次性凭证')
|
|
})
|
|
|
|
it('keeps review-only roles on the merged customer page without calling the customer list endpoint', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = ['platform:customer:review', 'platform:customer:approve']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listEnterpriseReviews).mockResolvedValue([pendingReview])
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
expect(listPlatformTenants).not.toHaveBeenCalled()
|
|
expect(document.body.textContent).toContain(fullTenant.tenantName)
|
|
expect(tableButtonByText('审核')).toBeDefined()
|
|
})
|
|
|
|
it('does not let an older review response restore stale actions', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:review', 'platform:customer:approve']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
let resolveFirst!: (value: EnterpriseReview[]) => void
|
|
let resolveSecond!: (value: EnterpriseReview[]) => void
|
|
vi.mocked(listEnterpriseReviews)
|
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve }))
|
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await Promise.resolve()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('.query-toolbar__actions button')]
|
|
.find((button) => button.textContent?.trim() === '查询')?.click()
|
|
await Promise.resolve()
|
|
|
|
resolveSecond([pendingReview])
|
|
await flushPromises()
|
|
expect(tableButtonByText('审核')).toBeDefined()
|
|
|
|
resolveFirst([])
|
|
await flushPromises()
|
|
expect(tableButtonByText('审核')).toBeDefined()
|
|
})
|
|
|
|
it('resets only the customer owner password through the tenant contract', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:reset-password']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [{ ...fullTenant, status: 'ACTIVE' }], page: 1, size: 10, total: 1, pages: 1 })
|
|
vi.mocked(resetPlatformTenantOwnerPassword).mockResolvedValue()
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
tableButtonByText('重置密码')?.click()
|
|
await flushPromises()
|
|
|
|
const password = document.querySelector<HTMLInputElement>('input[autocomplete="new-password"]')
|
|
const confirmation = document.querySelector<HTMLInputElement>('input[placeholder="请再次输入新密码"]')
|
|
password!.value = 'NewPassword#2026'
|
|
password!.dispatchEvent(new Event('input', { bubbles: true }))
|
|
confirmation!.value = 'NewPassword#2026'
|
|
confirmation!.dispatchEvent(new Event('input', { bubbles: true }))
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '确认重置')?.click()
|
|
await flushPromises()
|
|
|
|
expect(resetPlatformTenantOwnerPassword).toHaveBeenCalledWith(fullTenant.id, {
|
|
newPassword: 'NewPassword#2026', forceChangePassword: true,
|
|
})
|
|
})
|
|
|
|
it('archives a customer only after its customer ID is explicitly confirmed', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:delete']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(archivePlatformTenant).mockResolvedValue({ ...fullTenant, status: 'ARCHIVED' })
|
|
vi.mocked(listPlatformTenants)
|
|
.mockResolvedValueOnce({ records: [fullTenant], page: 1, size: 10, total: 1, pages: 1 })
|
|
.mockResolvedValueOnce({ records: [], page: 1, size: 10, total: 0, pages: 0 })
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
expect(document.querySelector('.el-table__body button[aria-label^="更多客户操作:"]')).toBeNull()
|
|
tableButtonByText('删除')?.click()
|
|
await flushPromises()
|
|
|
|
const confirmation = document.querySelector<HTMLInputElement>('input[placeholder="TEN8"]')
|
|
expect(confirmation).toBeTruthy()
|
|
expect([...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '确认删除')?.disabled).toBe(true)
|
|
|
|
confirmation!.value = 'TEN8'
|
|
confirmation!.dispatchEvent(new Event('input', { bubbles: true }))
|
|
await flushPromises()
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
.find((button) => button.textContent?.trim() === '确认删除')?.click()
|
|
await flushPromises()
|
|
|
|
expect(archivePlatformTenant).toHaveBeenCalledWith(fullTenant.id)
|
|
expect(document.body.textContent).not.toContain(fullTenant.tenantName)
|
|
})
|
|
|
|
it('keeps the first operation column unfixed so it cannot cover a 320px table viewport', async () => {
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
expect(document.querySelector('.el-table__fixed')).toBeNull()
|
|
})
|
|
|
|
it('keeps three high-frequency actions visible and moves overflow actions into a menu', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:reset-password', 'platform:customer:impersonate', 'platform:customer:status', 'platform:customer:delete']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [{ ...fullTenant, status: 'ACTIVE' }], page: 1, size: 10, total: 1, pages: 1 })
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
expect(tableButtonByText('详情')).toBeDefined()
|
|
expect(tableButtonByText('编辑')).toBeDefined()
|
|
expect(document.querySelector('.el-table__body button[aria-label^="更多客户操作:"]')).not.toBeNull()
|
|
expect(tableButtonByText('重置密码')).toBeDefined()
|
|
expect(tableButtonByText('跳转登录')).toBeUndefined()
|
|
await clickMoreAction('跳转登录')
|
|
expect(document.body.textContent).toContain('代登录原因')
|
|
})
|
|
|
|
it('renders one overflow menu per customer when permitted actions exceed three', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:reset-password', 'platform:customer:impersonate', 'platform:customer:status', 'platform:customer:delete']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({
|
|
records: [
|
|
{ ...fullTenant, id: '8', tenantName: '客户甲', tenantCode: 'TEN-A', status: 'ACTIVE' },
|
|
{ ...fullTenant, id: '9', tenantName: '', tenantCode: 'TEN-B', status: 'ACTIVE' },
|
|
{ ...fullTenant, id: '10', tenantName: '', tenantCode: '', status: 'ACTIVE' },
|
|
],
|
|
page: 1, size: 10, total: 3, pages: 1,
|
|
})
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
expect(document.querySelectorAll('.el-table__body button[aria-label^="更多客户操作:"]').length).toBe(3)
|
|
expect(document.querySelectorAll('.el-table__body .row-actions > .el-button').length).toBe(9)
|
|
})
|
|
|
|
it('keeps the action row usable when all customer identifiers are missing', async () => {
|
|
const auth = useAuthStore()
|
|
auth.permissions = [...auth.permissions, 'platform:customer:reset-password', 'platform:customer:impersonate', 'platform:customer:status', 'platform:customer:delete']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
vi.mocked(listPlatformTenants).mockResolvedValue({
|
|
records: [{ ...fullTenant, id: null, tenantName: '', tenantCode: '', status: 'ACTIVE' } as unknown as PlatformTenantResource],
|
|
page: 1, size: 10, total: 1, pages: 1,
|
|
})
|
|
|
|
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
await flushPromises()
|
|
|
|
expect(document.querySelector('.el-table__body button[aria-label="更多客户操作:当前客户"]')).not.toBeNull()
|
|
expect(tableButtonByText('详情')).toBeDefined()
|
|
})
|
|
})
|