2026-08-04 13:32:08 +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 {
|
|
|
|
|
downloadTenantQualification,
|
|
|
|
|
getPlatformTenant,
|
2026-08-07 09:06:22 +08:00
|
|
|
listPlatformTenantMembers,
|
2026-08-04 13:32:08 +08:00
|
|
|
listPlatformTenantOrderCounts,
|
|
|
|
|
listTenantQualifications,
|
|
|
|
|
type PlatformTenantResource,
|
|
|
|
|
} from '@/modules/iam'
|
2026-08-07 18:03:59 +08:00
|
|
|
import { listPlatformFirstLegOrders, type PlatformFirstLegOrderResource } from '@/modules/first-leg-orders'
|
2026-08-04 13:32:08 +08:00
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
|
|
|
import CustomerDetailView from './CustomerDetailView.vue'
|
|
|
|
|
|
2026-08-07 20:07:37 +08:00
|
|
|
const { routerPush, routeControl } = vi.hoisted(() => ({
|
|
|
|
|
routerPush: vi.fn(),
|
|
|
|
|
routeControl: { setTenantId: (_tenantId: string) => {} },
|
|
|
|
|
}))
|
2026-08-04 13:32:08 +08:00
|
|
|
|
|
|
|
|
vi.mock('vue-router', async (importOriginal) => ({
|
|
|
|
|
...await importOriginal<typeof import('vue-router')>(),
|
2026-08-07 20:07:37 +08:00
|
|
|
...await (async () => {
|
|
|
|
|
const { reactive } = await import('vue')
|
|
|
|
|
const route = reactive({ params: { tenantId: '8' } })
|
|
|
|
|
routeControl.setTenantId = (tenantId: string) => { route.params.tenantId = tenantId }
|
|
|
|
|
return { useRoute: () => route }
|
|
|
|
|
})(),
|
2026-08-04 13:32:08 +08:00
|
|
|
useRouter: () => ({ push: routerPush }),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
vi.mock('@/modules/iam', () => ({
|
|
|
|
|
changePlatformTenantStatus: vi.fn(),
|
|
|
|
|
downloadTenantQualification: vi.fn(),
|
|
|
|
|
getPlatformTenant: vi.fn(),
|
2026-08-07 09:06:22 +08:00
|
|
|
listPlatformTenantMembers: vi.fn(),
|
2026-08-04 13:32:08 +08:00
|
|
|
listPlatformTenantOrderCounts: vi.fn(),
|
|
|
|
|
listTenantQualifications: vi.fn(),
|
|
|
|
|
qualificationFileName: vi.fn((documentUrl: string) => documentUrl.split('/').at(-1) || '资质文件'),
|
|
|
|
|
}))
|
|
|
|
|
|
2026-08-07 18:03:59 +08:00
|
|
|
vi.mock('@/modules/first-leg-orders', () => ({
|
|
|
|
|
listPlatformFirstLegOrders: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
2026-08-04 13:32:08 +08:00
|
|
|
class ResizeObserverStub implements ResizeObserver {
|
|
|
|
|
observe() {}
|
|
|
|
|
unobserve() {}
|
|
|
|
|
disconnect() {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tenant: PlatformTenantResource = {
|
|
|
|
|
id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'ACTIVE', 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: '2026-08-02T10:00:00',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
|
|
|
|
|
|
describe('CustomerDetailView', () => {
|
|
|
|
|
let pinia: ReturnType<typeof createPinia>
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks()
|
2026-08-07 20:07:37 +08:00
|
|
|
routeControl.setTenantId('8')
|
2026-08-04 13:32:08 +08:00
|
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
2026-08-05 15:54:59 +08:00
|
|
|
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { configurable: true, value: vi.fn() })
|
2026-08-04 13:32:08 +08:00
|
|
|
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:view', 'platform:user:list', 'platform:order:list']
|
|
|
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
|
|
|
vi.mocked(getPlatformTenant).mockResolvedValue(tenant)
|
|
|
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([{
|
|
|
|
|
id: 'qualification-1', tenantId: tenant.id, qualificationType: 'BUSINESS_LICENSE', certificateNo: tenant.unifiedSocialCreditCode,
|
|
|
|
|
documentUrl: '/files/license.png', status: 'SUPERSEDED', reviewComment: '已替换', validTo: null,
|
|
|
|
|
}])
|
2026-08-07 09:06:22 +08:00
|
|
|
vi.mocked(listPlatformTenantMembers).mockResolvedValue([{
|
|
|
|
|
membershipId: 'member-1', userId: 'user-1', authUserId: 'auth-1', username: 'merchant_admin', displayName: '张三',
|
|
|
|
|
phone: '13800138000', email: 'ops@example.com', memberType: 'OWNER', memberNo: null, status: 'ACTIVE', roleIds: [], roleNames: ['企业管理员'],
|
|
|
|
|
}])
|
2026-08-04 13:32:08 +08:00
|
|
|
vi.mocked(listPlatformTenantOrderCounts).mockResolvedValue([{ tenantId: tenant.id, orderCount: 12 }])
|
2026-08-07 18:03:59 +08:00
|
|
|
vi.mocked(listPlatformFirstLegOrders).mockResolvedValue({
|
|
|
|
|
records: [{
|
|
|
|
|
id: 'order-1', tenantId: tenant.id, orderNo: 'BY20260807001', customerId: null, customerName: tenant.tenantName,
|
|
|
|
|
status: 'PROCESSING', transportMode: 'SEA', tradeTerm: null, destinationCountryCode: 'US', destinationWarehouseId: null,
|
|
|
|
|
destinationWarehouseName: '洛杉矶', expectedShipDate: '2026-08-08', customerRemark: null, pickupMethod: 'WAREHOUSE',
|
|
|
|
|
receivingWarehouseId: 'warehouse-1', receivingWarehouseName: '深圳头程收货仓', receivingWarehouseAddress: '深圳市', receivingTime: null,
|
|
|
|
|
channelCode: 'SEA_FCL', channelName: '海运整箱', taxMode: 'DDP', containerMode: 'FULL_CONTAINER', customsInspection: false,
|
|
|
|
|
destinationType: 'FBA', amazonWarehouseCode: null, shipmentId: null, referenceId: null, expressCompany: null, expressTrackingNo: null,
|
|
|
|
|
createdAt: '2026-08-07T10:00:00', updatedAt: '2026-08-07T10:00:00',
|
|
|
|
|
} satisfies PlatformFirstLegOrderResource], page: 1, size: 10, total: 1, pages: 1,
|
|
|
|
|
})
|
2026-08-04 13:32:08 +08:00
|
|
|
vi.mocked(downloadTenantQualification).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-07 09:06:22 +08:00
|
|
|
it('renders enterprise users and includes the owner in customer statistics', async () => {
|
2026-08-04 13:32:08 +08:00
|
|
|
mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect([...document.querySelectorAll('.detail-tabs button')].map((button) => button.textContent?.trim())).toEqual([
|
2026-08-11 19:59:15 +08:00
|
|
|
'基本信息', '业务概况', '运单记录', '计费规则',
|
2026-08-04 13:32:08 +08:00
|
|
|
])
|
2026-08-07 09:06:22 +08:00
|
|
|
expect(document.body.textContent).toContain('企业用户1人')
|
|
|
|
|
expect(document.body.textContent).toContain('1 人正常使用')
|
|
|
|
|
expect(document.body.textContent).toContain('merchant_admin')
|
|
|
|
|
expect(document.body.textContent).toContain('企业管理员')
|
|
|
|
|
expect(listPlatformTenantMembers).toHaveBeenCalledWith(tenant.id)
|
2026-08-04 13:32:08 +08:00
|
|
|
expect(document.body.textContent).toContain('已替换')
|
|
|
|
|
expect(document.body.textContent).toContain('12票')
|
2026-08-05 15:54:59 +08:00
|
|
|
|
2026-08-07 18:03:59 +08:00
|
|
|
const ordersTab = [...document.querySelectorAll<HTMLButtonElement>('.detail-tabs button')]
|
|
|
|
|
.find((button) => button.textContent?.trim() === '运单记录')
|
|
|
|
|
ordersTab?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(document.body.textContent).toContain('BY20260807001')
|
|
|
|
|
expect(document.body.textContent).toContain('海运整箱 FCL')
|
|
|
|
|
expect(listPlatformFirstLegOrders).toHaveBeenCalledWith({ tenantId: tenant.id, page: 1, size: 10 })
|
|
|
|
|
|
2026-08-05 15:54:59 +08:00
|
|
|
const billingTab = [...document.querySelectorAll<HTMLButtonElement>('.detail-tabs button')]
|
|
|
|
|
.find((button) => button.textContent?.trim() === '计费规则')
|
|
|
|
|
billingTab?.click()
|
|
|
|
|
await flushPromises()
|
2026-08-07 20:07:37 +08:00
|
|
|
expect(document.body.textContent).toContain('暂无尺寸计算规则查看权限')
|
2026-08-05 15:54:59 +08:00
|
|
|
expect(document.body.textContent).not.toContain('计费规则尚未接入')
|
2026-08-04 13:32:08 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-12 09:25:55 +08:00
|
|
|
it('does not expose unknown order status codes', async () => {
|
|
|
|
|
const result = await listPlatformFirstLegOrders({ tenantId: String(tenant.id), page: 1, size: 10 })
|
|
|
|
|
vi.mocked(listPlatformFirstLegOrders).mockResolvedValueOnce({
|
|
|
|
|
...result,
|
|
|
|
|
records: result.records.map((order) => ({ ...order, status: 'LEGACY_STATUS' as PlatformFirstLegOrderResource['status'] })),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const ordersTab = [...document.querySelectorAll<HTMLButtonElement>('.detail-tabs button')]
|
|
|
|
|
.find((button) => button.textContent?.trim() === '运单记录')
|
|
|
|
|
ordersTab?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(document.body.textContent).toContain('未知状态')
|
|
|
|
|
expect(document.body.textContent).not.toContain('LEGACY_STATUS')
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-04 13:32:08 +08:00
|
|
|
it('opens an authenticated qualification preview', async () => {
|
|
|
|
|
mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
const fileButton = document.querySelector<HTMLButtonElement>('.qualification-list button')
|
|
|
|
|
fileButton?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(downloadTenantQualification).toHaveBeenCalledWith(tenant.id, '/files/license.png')
|
|
|
|
|
expect(document.querySelector('img[alt="license.png预览"]')).not.toBeNull()
|
|
|
|
|
})
|
2026-08-07 18:15:56 +08:00
|
|
|
|
|
|
|
|
it('does not duplicate the first page request when retrying from a later page', async () => {
|
|
|
|
|
const wrapper = mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
vi.mocked(listPlatformFirstLegOrders).mockRejectedValueOnce(new Error('暂时不可用'))
|
|
|
|
|
|
|
|
|
|
const vm = wrapper.vm as unknown as { orderPage: number }
|
|
|
|
|
vm.orderPage = 2
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(listPlatformFirstLegOrders).toHaveBeenCalledTimes(2)
|
|
|
|
|
|
|
|
|
|
const ordersTab = [...document.querySelectorAll<HTMLButtonElement>('.detail-tabs button')]
|
|
|
|
|
.find((button) => button.textContent?.trim() === '运单记录')
|
|
|
|
|
ordersTab?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const retryButton = [...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
|
|
|
.find((button) => button.textContent?.includes('重新加载'))
|
|
|
|
|
retryButton?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(listPlatformFirstLegOrders).toHaveBeenCalledTimes(3)
|
|
|
|
|
expect(listPlatformFirstLegOrders).toHaveBeenLastCalledWith({ tenantId: tenant.id, page: 1, size: 10 })
|
|
|
|
|
})
|
2026-08-07 20:07:37 +08:00
|
|
|
|
|
|
|
|
it('reloads a reused detail view and ignores the previous customer response', async () => {
|
|
|
|
|
const secondTenant = { ...tenant, id: '9', tenantCode: 'TEN9', tenantName: '远航供应链', legalName: '远航供应链有限公司' }
|
|
|
|
|
let resolveFirstTenant!: (value: PlatformTenantResource) => void
|
|
|
|
|
const firstTenantRequest = new Promise<PlatformTenantResource>((resolve) => { resolveFirstTenant = resolve })
|
|
|
|
|
vi.mocked(getPlatformTenant).mockImplementation((tenantId) => tenantId === '8' ? firstTenantRequest : Promise.resolve(secondTenant))
|
|
|
|
|
vi.mocked(listTenantQualifications).mockResolvedValue([])
|
|
|
|
|
vi.mocked(listPlatformTenantMembers).mockResolvedValue([])
|
|
|
|
|
vi.mocked(listPlatformTenantOrderCounts).mockImplementation(async (tenantIds) => [{ tenantId: tenantIds[0], orderCount: 0 }])
|
|
|
|
|
vi.mocked(listPlatformFirstLegOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
|
|
|
|
|
|
|
|
|
|
mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
routeControl.setTenantId('9')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(getPlatformTenant).toHaveBeenCalledWith('9')
|
|
|
|
|
expect(document.body.textContent).toContain('远航供应链')
|
|
|
|
|
|
|
|
|
|
resolveFirstTenant(tenant)
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(document.body.textContent).toContain('远航供应链')
|
|
|
|
|
expect(document.body.textContent).not.toContain('柏悦物流')
|
|
|
|
|
})
|
2026-08-04 13:32:08 +08:00
|
|
|
})
|