This commit is contained in:
2026-08-04 09:05:19 +08:00
parent fa922ca96f
commit 815d4e8daf
21 changed files with 994 additions and 128 deletions
@@ -18,6 +18,7 @@ import {
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 CustomersView from './CustomersView.vue'
vi.mock('@/modules/iam', () => ({
@@ -47,7 +48,7 @@ class ResizeObserverStub implements ResizeObserver {
const fullTenant: PlatformTenantResource = {
id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'PROFILE_REQUIRED',
legalName: '柏悦物流有限公司', unifiedSocialCreditCode: '91440300MA5ABCDE12',
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,
@@ -83,7 +84,7 @@ describe('CustomersView', () => {
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = ['platform:customer:list', 'platform:customer:view', 'platform:customer:edit', 'platform:customer:export']
auth.permissions = ['platform:customer:list', 'platform:customer:view', 'platform:customer:edit', 'platform:customer:export', 'platform:download:list']
useAccessPreviewStore().initializeAfterLogin()
vi.mocked(listPlatformTenants).mockResolvedValue({
records: [{ ...fullTenant, contactPhone: '138****8000', contactEmail: 'o***@example.com' }],
@@ -132,6 +133,33 @@ describe('CustomersView', () => {
expect(preview?.src).toBe('blob:qualification-preview')
})
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('shows uploaded qualifications in customer detail and opens the full preview', async () => {
vi.mocked(listTenantQualifications).mockResolvedValue([{
id: 'qualification-1', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
@@ -154,6 +182,52 @@ describe('CustomersView', () => {
expect(document.querySelector('.qualification-preview img[alt="license.png预览"]')).not.toBeNull()
})
it('does not let an older customer detail response overwrite a newer one', async () => {
const secondTenant = {
...fullTenant, id: '9', tenantCode: 'TEN9', tenantName: '第二家客户', legalName: '第二家客户有限公司',
}
vi.mocked(listPlatformTenants).mockResolvedValue({
records: [fullTenant, secondTenant], page: 1, size: 10, total: 2, pages: 1,
})
let resolveFirst!: (value: PlatformTenantResource) => void
let resolveSecond!: (value: PlatformTenantResource) => void
vi.mocked(getPlatformTenant)
.mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve }))
.mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
const detailButtons = [...document.querySelectorAll<HTMLButtonElement>('.el-table__body button')]
.filter((button) => button.textContent?.trim() === '详情')
detailButtons[0]?.click()
detailButtons[1]?.click()
resolveSecond(secondTenant)
await flushPromises()
expect(document.querySelector('.customer-detail')?.textContent).toContain('第二家客户有限公司')
resolveFirst(fullTenant)
await flushPromises()
const detailText = document.querySelector('.customer-detail')?.textContent
expect(detailText).toContain('第二家客户有限公司')
expect(detailText).not.toContain('柏悦物流有限公司')
})
it('labels replaced qualification history without presenting it as pending review', async () => {
vi.mocked(listTenantQualifications).mockResolvedValue([{
id: 'qualification-old', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/old-license.pdf',
status: 'SUPERSEDED', reviewComment: '已替换', validTo: null,
}])
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
tableButtonByText('详情')?.click()
await flushPromises()
expect(document.querySelector('.detail-qualification-meta .el-tag')?.textContent).toContain('已替换')
})
it('saves customer data and selected qualifications with one action', async () => {
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
@@ -183,9 +257,12 @@ describe('CustomersView', () => {
})
it('submits the current filters to the backend export service without loading every customer page', async () => {
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
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()
@@ -193,11 +270,35 @@ describe('CustomersView', () => {
expect(createPlatformExportTask).toHaveBeenCalledWith({
exportType: 'CUSTOMER_LIST', format: 'XLSX', taskName: '客户列表',
parameters: { keyword: undefined, status: undefined },
parameters: { keyword: 'Acme', status: 'ACTIVE' },
})
expect(listPlatformTenants).toHaveBeenCalledTimes(1)
})
it('hides customer export when the stable export permission is absent', async () => {
const auth = useAuthStore()
auth.permissions = ['platform:customer:list', 'platform:customer:view']
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']
@@ -228,6 +329,35 @@ describe('CustomersView', () => {
expect(document.body.textContent).toContain('Temporary#2026')
})
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']