2026-08-03 14:58:17 +08:00
|
|
|
import ElementPlus from 'element-plus'
|
|
|
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
|
|
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import {
|
2026-08-03 20:50:18 +08:00
|
|
|
approveEnterpriseReview,
|
2026-08-03 17:44:54 +08:00
|
|
|
downloadTenantQualification,
|
2026-08-03 14:58:17 +08:00
|
|
|
getPlatformTenant,
|
2026-08-03 20:50:18 +08:00
|
|
|
listEnterpriseReviews,
|
2026-08-03 14:58:17 +08:00
|
|
|
listPlatformTenants,
|
|
|
|
|
listTenantQualifications,
|
2026-08-03 20:50:18 +08:00
|
|
|
rejectEnterpriseReview,
|
2026-08-03 17:44:54 +08:00
|
|
|
updatePlatformTenant,
|
|
|
|
|
uploadPlatformTenantQualification,
|
2026-08-03 20:50:18 +08:00
|
|
|
type EnterpriseReview,
|
2026-08-03 14:58:17 +08:00
|
|
|
type PlatformTenantResource,
|
2026-08-03 18:49:18 +08:00
|
|
|
} from '@/modules/iam'
|
|
|
|
|
import { createPlatformExportTask } from '@/modules/downloads'
|
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
import { useAuthStore } from '@/core/auth/auth'
|
2026-08-03 14:58:17 +08:00
|
|
|
import CustomersView from './CustomersView.vue'
|
|
|
|
|
|
2026-08-03 18:49:18 +08:00
|
|
|
vi.mock('@/modules/iam', () => ({
|
2026-08-03 20:50:18 +08:00
|
|
|
approveEnterpriseReview: vi.fn(),
|
2026-08-03 14:58:17 +08:00
|
|
|
changePlatformTenantStatus: vi.fn(),
|
|
|
|
|
createPlatformTenant: vi.fn(),
|
2026-08-03 17:44:54 +08:00
|
|
|
downloadTenantQualification: vi.fn(),
|
2026-08-03 14:58:17 +08:00
|
|
|
getPlatformTenant: vi.fn(),
|
2026-08-03 20:50:18 +08:00
|
|
|
listEnterpriseReviews: vi.fn(),
|
2026-08-03 14:58:17 +08:00
|
|
|
listPlatformTenantOrderCounts: vi.fn(),
|
|
|
|
|
listPlatformTenants: vi.fn(),
|
|
|
|
|
listTenantQualifications: vi.fn(),
|
2026-08-03 17:44:54 +08:00
|
|
|
qualificationFileName: vi.fn((documentUrl: string) => documentUrl.split('/').at(-1) || '资质文件'),
|
2026-08-03 20:50:18 +08:00
|
|
|
rejectEnterpriseReview: vi.fn(),
|
2026-08-03 14:58:17 +08:00
|
|
|
submitPlatformTenantReview: vi.fn(),
|
|
|
|
|
updatePlatformTenant: vi.fn(),
|
|
|
|
|
uploadPlatformTenantQualification: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
2026-08-03 18:49:18 +08:00
|
|
|
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
2026-08-03 18:15:41 +08:00
|
|
|
|
2026-08-03 14:58:17 +08:00
|
|
|
class ResizeObserverStub implements ResizeObserver {
|
|
|
|
|
observe() {}
|
|
|
|
|
unobserve() {}
|
|
|
|
|
disconnect() {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fullTenant: PlatformTenantResource = {
|
|
|
|
|
id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'PROFILE_REQUIRED',
|
|
|
|
|
legalName: '柏悦物流有限公司', 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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 20:50:18 +08:00
|
|
|
const pendingReview: EnterpriseReview = {
|
|
|
|
|
id: '190000000000000001',
|
|
|
|
|
tenantId: fullTenant.id,
|
|
|
|
|
tenantName: fullTenant.tenantName,
|
|
|
|
|
tenantCode: fullTenant.tenantCode,
|
|
|
|
|
status: 'PENDING',
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 14:58:17 +08:00
|
|
|
function tableButtonByText(text: string) {
|
|
|
|
|
return [...document.querySelectorAll<HTMLButtonElement>('.el-table__body button')]
|
|
|
|
|
.find((button) => button.textContent?.trim() === text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
|
|
|
|
|
|
describe('CustomersView', () => {
|
|
|
|
|
let pinia: ReturnType<typeof createPinia>
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2026-08-03 17:44:54 +08:00
|
|
|
vi.clearAllMocks()
|
2026-08-03 14:58:17 +08:00
|
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
2026-08-03 17:44:54 +08:00
|
|
|
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:qualification-preview') })
|
|
|
|
|
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
|
2026-08-03 14:58:17 +08:00
|
|
|
document.body.innerHTML = ''
|
|
|
|
|
pinia = createPinia()
|
|
|
|
|
setActivePinia(pinia)
|
|
|
|
|
const auth = useAuthStore()
|
|
|
|
|
auth.token = 'token'
|
2026-08-03 18:15:41 +08:00
|
|
|
auth.permissions = ['platform:customer:list', 'platform:customer:view', 'platform:customer:edit', 'platform:customer:export']
|
2026-08-03 14:58:17 +08:00
|
|
|
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([])
|
2026-08-03 20:50:18 +08:00
|
|
|
vi.mocked(listEnterpriseReviews).mockResolvedValue([])
|
2026-08-03 17:44:54 +08:00
|
|
|
vi.mocked(updatePlatformTenant).mockResolvedValue(fullTenant)
|
|
|
|
|
vi.mocked(uploadPlatformTenantQualification).mockResolvedValue({
|
|
|
|
|
qualificationId: 'qualification-1', reviewId: null, tenantStatus: 'PROFILE_REQUIRED', documentUrl: '/files/license.png',
|
|
|
|
|
})
|
2026-08-03 18:15:41 +08:00
|
|
|
vi.mocked(createPlatformExportTask).mockResolvedValue({ id: 'export-1', status: 'PENDING' } as never)
|
2026-08-03 14:58:17 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
})
|
2026-08-03 17:44:54 +08:00
|
|
|
|
|
|
|
|
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('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',
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
const detailPreview = document.querySelector<HTMLButtonElement>('button[aria-label="查看营业执照:license.png"]')
|
|
|
|
|
expect(detailPreview?.querySelector('img[alt="营业执照缩略图"]')).not.toBeNull()
|
|
|
|
|
detailPreview?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(document.querySelector('.qualification-preview img[alt="license.png预览"]')).not.toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('saves customer data and selected qualifications with one action', async () => {
|
|
|
|
|
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 licenseInput = document.querySelector<HTMLInputElement>('.qualification-file input[type="file"]')
|
|
|
|
|
Object.defineProperty(licenseInput, 'files', { configurable: true, value: [licenseFile] })
|
|
|
|
|
licenseInput?.dispatchEvent(new Event('change', { bubbles: true }))
|
|
|
|
|
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(uploadPlatformTenantQualification).toHaveBeenCalledWith(
|
|
|
|
|
fullTenant.id,
|
|
|
|
|
'BUSINESS_LICENSE',
|
|
|
|
|
licenseFile,
|
|
|
|
|
undefined,
|
|
|
|
|
fullTenant.unifiedSocialCreditCode,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
})
|
2026-08-03 18:15:41 +08:00
|
|
|
|
|
|
|
|
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] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
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: undefined, status: undefined },
|
|
|
|
|
})
|
|
|
|
|
expect(listPlatformTenants).toHaveBeenCalledTimes(1)
|
|
|
|
|
})
|
2026-08-03 20:50:18 +08:00
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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('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()
|
|
|
|
|
})
|
2026-08-03 14:58:17 +08:00
|
|
|
})
|