import ElementPlus from 'element-plus' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getLastMileOrder } from '../api/lastMileOrders' import LastMileOrderDetailView from './LastMileOrderDetailView.vue' import { getWarehouseTransfer } from '../api/warehouseTransfers' const push = vi.hoisted(() => vi.fn()) vi.mock('vue-router', () => ({ useRoute: () => ({ params: { orderId: 'internal-order-id' } }), useRouter: () => ({ push }) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => false }) })) vi.mock('../api/lastMileOrders', () => ({ getLastMileOrder: vi.fn(), createLastMileOrderFee: vi.fn() })) vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn() })) enableAutoUnmount(afterEach) describe('LastMileOrderDetailView domestic detail alignment', () => { it.each(['PENDING_REVIEW', 'PARTIALLY_RECEIVED', 'COMPLETED'])('shows only customer SKU forecast regardless of receipt status (%s)', async status => { vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'return-tail', customerId: 'C1', orderType: 'RETURN_RELABEL', orderNo: 'RT-1', currentStatus: status, packageCount: 30 }, cargo: [], fees: [], statusHistory: [], operationHistory: [], returnReceipt: { order: { returnNo: 'RT-1', status, expectedQuantity: 30, receivedQuantity: 5 }, receipts: [], items: [{ skuCode: 'SKU-FORECAST', productNameZh: '预报商品', expectedQuantity: 30, receivedQuantity: 5 }] } }) const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises() expect(wrapper.get('[aria-label="基本信息"]').text()).not.toContain('箱数') await wrapper.findAll('[role="tab"]').find(tab => tab.text().includes('货物信息'))!.trigger('click'); await flushPromises() const panel = wrapper.get('.table-panel') expect(panel.text()).toContain('SKU-FORECAST') expect(panel.text()).toContain('预报商品') expect(panel.text()).toContain('预报件数') expect(panel.text()).toContain('30') expect(panel.text()).not.toContain('箱数') }) it('keeps customer forecast after completion instead of using actual signed cargo', async () => { vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'return-tail', customerId: 'C1', orderType: 'RETURN_RELABEL', orderNo: 'RT-1', currentStatus: 'INBOUND_COMPLETED' }, cargo: [{ id: 'actual', skuCode: 'SKU-ACTUAL', productNameZh: '实收商品', productQuantity: 25, packageCount: 2 }], fees: [], statusHistory: [], operationHistory: [], returnReceipt: { order: { returnNo: 'RT-1', status: 'COMPLETED', expectedQuantity: 30, receivedQuantity: 25 }, receipts: [], items: [{ skuCode: 'SKU-FORECAST', expectedQuantity: 30, receivedQuantity: 25 }] } }) const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises() await wrapper.findAll('[role="tab"]').find(tab => tab.text().includes('货物信息'))!.trigger('click'); await flushPromises() expect(wrapper.get('.table-panel').text()).not.toContain('SKU-ACTUAL') expect(wrapper.get('.table-panel').text()).toContain('SKU-FORECAST') expect(wrapper.get('.table-panel').text()).toContain('30') expect(wrapper.get('.table-panel').text()).not.toContain('箱数') }) it('shows signed box contents and actual quantities on return orders', async () => { vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'tail-return', customerId: 'customer-1', orderNo: 'RETU-1', orderType: 'RETURN_RELABEL', currentStatus: 'INBOUND_COMPLETED' }, cargo: [], fees: [], statusHistory: [], operationHistory: [], returnReceipt: { order: { returnNo: 'RT-1', receivedQuantity: 10, expectedQuantity: 10 }, receipts: [{ receiptNo: 'RR-1', unitType: 'PALLET', unitCode: 'PLT-1', receivedQuantity: 10, boxCount: 2, signedAt: '' }], cartons: [{ receiptNo: 'RR-1', cartonMark: 'RC#2-1', countStatus: 'CONFIRMED', items: [{ skuCode: 'SKU-A', quantity: 3 }, { skuCode: 'SKU-B', quantity: 7 }] }, { receiptNo: 'RR-2', cartonMark: 'RC-PENDING', countStatus: 'PENDING', items: [] }] } }) const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises() await wrapper.findAll('[role="tab"]').find(tab => tab.text().includes('退货签收与箱内清点'))!.trigger('click'); await flushPromises() const panel = wrapper.get('[aria-label="退货签收与箱内清点"]') expect(panel.text()).toContain('实收 10 / 预报 10 件') expect(panel.text()).toContain('RC#2-1') expect(panel.text()).toContain('SKU-A') expect(panel.text()).toContain('实际签收数量') expect(panel.text()).toContain('待清点') }) it('loads transfer fulfillment only on the plan tab using sourceOrderNo, never the tail order number', async () => { vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'internal-transfer-tail', customerId: 'customer-1', orderNo: 'WT-TAIL-1', sourceOrderNo: 'WT-SOURCE-1', orderType: 'WAREHOUSE_TRANSFER', currentStatus: 'INBOUND_COMPLETED' }, cargo: [], fees: [], statusHistory: [], operationHistory: [] }) vi.mocked(getWarehouseTransfer).mockResolvedValue({ id: 'internal-transfer', transferNo: 'WT-SOURCE-1', needRelabel: true, status: 'WAIT_RELABEL', relabelNo: 'RL-1', relabelStatus: 'CUSTOMER_CONFIRM_PENDING', packageCount: 2, items: [], cartons: [], pallets: [], relabelBindings: [] }) const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises() expect(getWarehouseTransfer).not.toHaveBeenCalled() await wrapper.findAll('[role="tab"]').find(x => x.text().includes('计划信息'))!.trigger('click'); await flushPromises() expect(getWarehouseTransfer).toHaveBeenCalledExactlyOnceWith('customer-1', 'WT-SOURCE-1') expect(wrapper.get('[aria-label="本票转运与换标履约"]').text()).toContain('待客户确认') expect(wrapper.get('[aria-label="本票转运与换标履约"]').text()).toContain('不得提前出库') }) beforeEach(() => { push.mockReset() vi.mocked(getWarehouseTransfer).mockReset() vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'internal-order-id', customerId: 'customer-1', orderNo: 'FL-WH-001', currentStatus: 'PENDING_ASSIGNMENT', customerName: '示例客户', overseasWarehouseName: '洛杉矶仓', packageCount: 0, palletCount: 2, grossWeightKg: 30, netWeightKg: 22, volumeCbm: 6.374, inboundCode: 'IN-001', containerNo: 'CONT-001', customerRemark: '完整保留客户备注', updatedAt: '2026-09-14T10:00:00' }, cargo: [{ id: 'cargo-1', skuCode: 'SKU-001', productNameZh: '测试货物' }], fees: [], statusHistory: [], operationHistory: [], }) }) it('shows business references without changing the transfer workflow or requiring business-list permission', async () => { vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'internal-order-id', customerId: 'A', orderNo: 'WT-1', sourceOrderNo: 'WT-1', orderType: 'WAREHOUSE_TRANSFER', businessSource: { businessType: 'LOCAL_LOOSEBOARD', label: '本土散板 · 拼车需求池', businessOrderNo: 'LB-1', externalBookingNo: 'FB-1' } }, cargo: [], fees: [], statusHistory: [], operationHistory: [] }) const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises() const source = wrapper.get('[aria-label="商业来源与关联单据"]') expect(source.text()).toContain('本土散板 · 拼车需求池') expect(source.text()).toContain('LB-1') expect(source.text()).toContain('FB-1') expect(source.find('button').exists()).toBe(false) expect(wrapper.get('[aria-label="基本信息"]').text()).toContain('履约类型仓储转运') expect(getWarehouseTransfer).not.toHaveBeenCalled() }) it('shows one business heading, a semantic status and summary without losing basic fields', async () => { const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }) await flushPromises() expect(wrapper.findAll('h1')).toHaveLength(1) expect(wrapper.get('h1').text()).toBe('FL-WH-001') const header = wrapper.get('.density-detail-header') expect(header.text()).not.toContain('尾程订单详情') expect(header.text()).not.toContain('尾程单号') expect(header.get('.status-badge').classes()).toContain('is-warning') expect(header.get('.order-reference-text__copy').classes()).toContain('is-text') const summary = wrapper.get('[aria-label="尾程订单概览"]') expect(summary.findAll('dd').map(item => item.text())).toEqual(['示例客户', '洛杉矶仓', '0 箱', '2 托', '30 kg', '22 kg', '6.374 m³', expect.stringContaining('2026-09-14')]) const fields = wrapper.get('dl[aria-label="基本信息"]') expect(fields.findAll('dt')).toHaveLength(22) expect(fields.text()).toContain('货物毛重(kg)30') expect(fields.text()).toContain('货物净重(kg)22') expect(fields.text()).toContain('CONT-001') expect(fields.text()).toContain('完整保留客户备注') expect(fields.findAll('.wide')).toHaveLength(3) expect(wrapper.text()).not.toContain('internal-order-id') await header.get('.density-back').trigger('click') expect(push).toHaveBeenCalledWith({ name: 'overseas-last-mile-orders' }) }) it('keeps all five tab panels reachable after applying the shared tab style', async () => { const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }) await flushPromises() const tabs = wrapper.findAll('[role="tab"]') expect(tabs.map(tab => tab.text())).toEqual(['📋基本信息', '📦货物信息', '🗓️计划信息', '💰费用明细', '📝操作记录']) for (const [index, tab] of tabs.entries()) { await tab.trigger('click') await flushPromises() expect(tab.attributes('aria-selected')).toBe('true') const panel = wrapper.findAll('[role="tabpanel"]')[index]! expect(panel.isVisible()).toBe(true) if (index === 1) expect(panel.text()).toContain('SKU-001') } }) it('shows declared, received and highlighted signed difference only for warehouse transfers', async () => { vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'transfer-id', customerId: 'customer-1', orderNo: 'WT-001', orderType: 'WAREHOUSE_TRANSFER', currentStatus: 'WAIT_CUSTOMER_CONFIRMATION', packageCount: 10, receivedPackageCount: 12, packageDifference: 2 }, cargo: [], fees: [], statusHistory: [], operationHistory: [], }) const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }) await flushPromises() const fields = wrapper.get('dl[aria-label="基本信息"]') expect(fields.text()).toContain('客户预报箱数10') expect(fields.text()).toContain('实收箱数12') expect(fields.text()).toContain('箱数差异+2') expect(fields.get('.package-difference--nonzero').text()).toBe('+2') expect(wrapper.get('.status-badge').text()).toBe('待客户确认实测') const overview = wrapper.get('[aria-label="尾程订单概览"]') expect(overview.text()).toContain('实收总箱数12 箱') expect(overview.text()).not.toContain('客户预报总箱数') }) it('shows the declared total before a warehouse transfer has a received count', async () => { vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'transfer-id', customerId: 'customer-1', orderNo: 'WT-002', orderType: 'WAREHOUSE_TRANSFER', currentStatus: 'WAIT_PDA_RECEIVING', packageCount: 10, receivedPackageCount: null, packageDifference: null }, cargo: [], fees: [], statusHistory: [], operationHistory: [], }) const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }) await flushPromises() const overview = wrapper.get('[aria-label="尾程订单概览"]') expect(overview.text()).toContain('客户预报总箱数10 箱') expect(overview.text()).not.toContain('实收总箱数') }) it('renders both return relabel documents with independent states and quantities', async () => { vi.mocked(getLastMileOrder).mockResolvedValue({ order: { id: 'return-id', customerId: 'customer-1', orderNo: 'RT-001', orderType: 'RETURN_RELABEL', currentStatus: 'INBOUND_COMPLETED', relabelOrderNo: 'RL-SECOND', relabelStatus: 'PROCESSING', relabelApplications: [ { relabelNo: 'RL-FIRST', status: 'COMPLETED', quantity: 10, createdAt: '2026-09-21T10:00:00' }, { relabelNo: 'RL-SECOND', status: 'PROCESSING', quantity: 5, createdAt: '2026-09-21T11:00:00' }, ], }, cargo: [], fees: [], statusHistory: [], operationHistory: [], }) const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [ElementPlus] } }) await flushPromises() await wrapper.get('#tab-plan').trigger('click') await flushPromises() const rows = wrapper.findAll('.related-panel .el-table__body tbody tr') expect(rows).toHaveLength(2) expect(rows[0].text()).toContain('RL-FIRST') expect(rows[0].text()).toContain('已完成') expect(rows[0].text()).toContain('10') expect(rows[1].text()).toContain('RL-SECOND') expect(rows[1].text()).toContain('处理中') expect(rows[1].text()).toContain('5') }) })