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, listPlatformTenantMembers, listTenantQualifications, type PlatformTenantResource, } from '@/modules/iam' import { getPlatformTenantOrderStatistics, listPlatformFirstLegOrders, type PlatformFirstLegOrderResource } from '@/modules/first-leg-orders' import { getFinanceCustomerAccountSummary } from '@/modules/finance' import { getCustomerContainerTransitSummary } from '@/modules/container-loading' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' import CustomerDetailView from './CustomerDetailView.vue' const { routerPush, routeControl } = vi.hoisted(() => ({ routerPush: vi.fn(), routeControl: { setTenantId: (_tenantId: string) => {} }, })) vi.mock('vue-router', async (importOriginal) => ({ ...await importOriginal(), ...await (async () => { const { reactive } = await import('vue') const route = reactive({ params: { tenantId: '8' } }) routeControl.setTenantId = (tenantId: string) => { route.params.tenantId = tenantId } return { useRoute: () => route } })(), useRouter: () => ({ push: routerPush }), })) vi.mock('@/modules/iam', () => ({ changePlatformTenantStatus: vi.fn(), downloadTenantQualification: vi.fn(), getPlatformTenant: vi.fn(), listPlatformTenantMembers: vi.fn(), listTenantQualifications: vi.fn(), qualificationFileName: vi.fn((documentUrl: string) => documentUrl.split('/').at(-1) || '资质文件'), })) vi.mock('@/modules/first-leg-orders', () => ({ getPlatformTenantOrderStatistics: vi.fn(), listPlatformFirstLegOrders: vi.fn(), })) vi.mock('@/modules/finance', () => ({ CustomerStatementsPanel: { template: '
财务账单
' }, getFinanceCustomerAccountSummary: vi.fn(), })) vi.mock('@/modules/container-loading', () => ({ getCustomerContainerTransitSummary: vi.fn() })) 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 beforeEach(() => { vi.clearAllMocks() routeControl.setTenantId('8') vi.stubGlobal('ResizeObserver', ResizeObserverStub) Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { configurable: true, value: vi.fn() }) 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:user:list', 'platform:order:list', 'platform:finance:customer-account:list', 'platform:firstleg:container: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, }]) 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: ['企业管理员'], }]) vi.mocked(getPlatformTenantOrderStatistics).mockResolvedValue({ tenantId: String(tenant.id), totalSubmittedOrders: 12, currentMonthSubmittedOrders: 3, monthStart: '2026-08-01T00:00:00+08:00', nextMonthStart: '2026-09-01T00:00:00+08:00', zoneId: 'Asia/Shanghai', asOf: '2026-08-13T12:00:00+08:00' }) vi.mocked(getFinanceCustomerAccountSummary).mockResolvedValue({ customerId: String(tenant.id), currency: 'CNY', totalReceivable: '123456.78', receivedAmount: '100000', outstandingReceivable: '23456.78', pendingBilling: '0', outstandingPayable: '0', asOf: '2026-08-13T04:00:00Z', lastChangedAt: null }) vi.mocked(getCustomerContainerTransitSummary).mockResolvedValue({ customerId: String(tenant.id), containerCount: 2, totalTeu: '2.25', unknownContainerCount: 0, asOf: '2026-08-13T12:00:00', definition: '已开船未到仓' }) 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, }) vi.mocked(downloadTenantQualification).mockResolvedValue(new Blob(['image'], { type: 'image/png' })) }) it('renders three operating metrics and keeps enterprise users in detail content', async () => { mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() expect([...document.querySelectorAll('.detail-tabs button')].map((button) => button.getAttribute('aria-label'))).toEqual([ '基本信息', '业务概况', '运单记录', '财务账单', '计费规则', '操作日志', ]) expect(document.body.textContent).toContain('累计运单12票本月 3 票') expect(document.body.textContent).toContain('累计应收12.345678万元未结清 2.345678 万元') expect(document.body.textContent).toContain('在途柜量2.25TEU2 柜') expect([...document.querySelectorAll('.overview-strip article')]).toHaveLength(3) expect(document.querySelector('.overview-strip')?.textContent).not.toContain('企业用户') expect(document.body.textContent).toContain('merchant_admin') expect(document.body.textContent).toContain('企业管理员') expect(listPlatformTenantMembers).toHaveBeenCalledWith(tenant.id) expect(document.body.textContent).toContain('已替换') expect(getPlatformTenantOrderStatistics).toHaveBeenCalledWith(tenant.id) expect(getFinanceCustomerAccountSummary).toHaveBeenCalledWith(tenant.id) expect(getCustomerContainerTransitSummary).toHaveBeenCalledWith(tenant.id) const ordersTab = document.querySelector('.detail-tabs button[aria-label="运单记录"]') 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 }) const billingTab = document.querySelector('.detail-tabs button[aria-label="计费规则"]') billingTab?.click() await flushPromises() expect(document.body.textContent).toContain('暂无尺寸计算规则查看权限') expect(document.body.textContent).not.toContain('计费规则尚未接入') }) 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.querySelector('.detail-tabs button[aria-label="运单记录"]') ordersTab?.click() await flushPromises() expect(document.body.textContent).toContain('未知状态') expect(document.body.textContent).not.toContain('LEGACY_STATUS') }) it('opens an authenticated qualification preview', async () => { mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() const fileButton = document.querySelector('.qualification-list button') fileButton?.click() await flushPromises() expect(downloadTenantQualification).toHaveBeenCalledWith(tenant.id, '/files/license.png') expect(document.querySelector('img[alt="license.png预览"]')).not.toBeNull() }) 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.querySelector('.detail-tabs button[aria-label="运单记录"]') ordersTab?.click() await flushPromises() const retryButton = [...document.querySelectorAll('button')] .find((button) => button.textContent?.includes('重新加载')) retryButton?.click() await flushPromises() expect(listPlatformFirstLegOrders).toHaveBeenCalledTimes(3) expect(listPlatformFirstLegOrders).toHaveBeenLastCalledWith({ tenantId: tenant.id, page: 1, size: 10 }) }) 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((resolve) => { resolveFirstTenant = resolve }) vi.mocked(getPlatformTenant).mockImplementation((tenantId) => tenantId === '8' ? firstTenantRequest : Promise.resolve(secondTenant)) vi.mocked(listTenantQualifications).mockResolvedValue([]) vi.mocked(listPlatformTenantMembers).mockResolvedValue([]) vi.mocked(getPlatformTenantOrderStatistics).mockImplementation(async (tenantId) => ({ tenantId, totalSubmittedOrders: 0, currentMonthSubmittedOrders: 0, monthStart: '2026-08-01', nextMonthStart: '2026-09-01', zoneId: 'Asia/Shanghai', asOf: '2026-08-13' })) 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('柏悦物流') }) it('does not request metrics without their independent permissions', async () => { const auth = useAuthStore() auth.permissions = ['platform:customer:list', 'platform:user:list'] useAccessPreviewStore().initializeAfterLogin() mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() expect(getPlatformTenantOrderStatistics).not.toHaveBeenCalled() expect(getFinanceCustomerAccountSummary).not.toHaveBeenCalled() expect(getCustomerContainerTransitSummary).not.toHaveBeenCalled() expect(document.querySelector('.overview-strip')?.textContent?.match(/暂无查看权限/g)).toHaveLength(3) }) it('keeps metric failures independent and never renders them as zero', async () => { vi.mocked(getFinanceCustomerAccountSummary).mockRejectedValueOnce(new Error('finance unavailable')) mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() const cards = [...document.querySelectorAll('.overview-strip article')] expect(cards[0]?.textContent).toContain('12票') expect(cards[1]?.textContent).toContain('—加载失败') expect(cards[1]?.textContent).not.toContain('0万元') expect(cards[2]?.textContent).toContain('2.25TEU') }) })