import ElementPlus from 'element-plus' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import InboundOrdersView from './InboundOrdersView.vue' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import QueryToolbar from '@/shared/components/QueryToolbar.vue' import DataTable from '@/shared/components/DataTable.vue' import { listPlatformWarehouses } from '@/modules/warehouses' import { useAuthStore } from '@/core/auth/auth' import { downloadInboundCartonLabels, listPlatformInbounds } from '../core/api' import type { InboundOrderSummary } from '../core/types' const routerPush = vi.fn() vi.mock('vue-router', async (importOriginal) => ({ ...await importOriginal(), useRouter: () => ({ push: routerPush }), })) vi.mock('../core/api', () => ({ downloadInboundCartonLabels: vi.fn(), listPlatformInbounds: vi.fn(), })) vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: vi.fn(), })) vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn(), })) const customers: PlatformTenantResource[] = [ { id: '10001', tenantCode: 'CUS-001', tenantName: '示例企业', status: 'ACTIVE', legalName: '示例企业有限公司', legalRepresentative: null, unifiedSocialCreditCode: null, contactName: null, contactPhone: null, contactEmail: null, servicePackage: null, contactWechat: null, salesOwnerName: null, paymentTermDays: null, enterpriseType: null, establishedDate: null, registeredCapital: null, registeredCapitalCurrency: null, registeredAddress: null, internalRemark: null, createdAt: null, approvedAt: null }, { id: '10002', tenantCode: 'CUS-002', tenantName: '第二客户', status: 'ACTIVE', legalName: null, legalRepresentative: null, unifiedSocialCreditCode: null, contactName: null, contactPhone: null, contactEmail: null, servicePackage: null, contactWechat: null, salesOwnerName: null, paymentTermDays: null, enterpriseType: null, establishedDate: null, registeredCapital: null, registeredCapitalCurrency: null, registeredAddress: null, internalRemark: null, createdAt: null, approvedAt: null }, ] const summary: InboundOrderSummary = { id: '19001', tenantId: '10002', orderId: '18001', orderNo: 'ORD-001', inboundCode: 'RK-001', processStatus: 'WAIT_MEASURE', orderStatusSnapshot: 'APPROVED', measureReviewStatus: 'PENDING', feeStatus: 'UNPRICED', feeReviewStatus: null, notificationStatus: null, version: 1, expectedArrivalAt: '2026-08-04T10:00:00', customerId: '10001', customerName: '示例客户', companyId: '10001', companyName: '示例企业', warehouseId: '20001', warehouseName: '义乌收货仓', deliveryWarehouseId: null, deliveryWarehouseName: null, businessType: 'FBA', channelCode: 'SEA', channelName: '海运', taxMode: 'DDP', containerMode: null, customsInspection: false, destinationType: 'FBA', destinationWarehouseId: '30001', destinationCode: 'ONT8', destinationName: 'ONT8', holdFlag: false, currentHoldReason: null, totalPackageCount: 6, actualGrossWeightKg: 120, actualNetWeightKg: 110, actualVolumeCbm: 1.2, volumeWeightKg: 200, chargeableWeightKg: 200, chargeableVolumeCbm: null, palletCount: 1, locationCodes: 'A-01', measureCompletedAt: null, inboundedAt: null, expressCompanyCode: null, expressCompanyName: null, expressTrackingNo: null, loadingStatus: null, containerNo: null, cabinetNo: null, vesselName: null, voyageNo: null, sailingAt: null, portArrivalAt: null, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-01T10:00:00', } enableAutoUnmount(afterEach) function mountPage() { return mount(InboundOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] }, }) } async function selectCustomer(wrapper: ReturnType, customerId = '10001') { await flushPromises() const toolbar = wrapper.findComponent(QueryToolbar) toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), customerId }) await flushPromises() return toolbar } describe('InboundOrdersView', () => { beforeEach(() => { vi.clearAllMocks() setActivePinia(createPinia()) useAuthStore().permissions = ['platform:firstleg:inbound:view'] vi.mocked(listPlatformInbounds).mockResolvedValue({ records: [summary], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(downloadInboundCartonLabels).mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' })) vi.mocked(listPlatformTenants).mockResolvedValue({ records: customers, page: 1, size: 200, total: 2, pages: 1 }) vi.mocked(listPlatformWarehouses).mockResolvedValue({ records: [ { id: '20001', tenantId: '10001', warehouseCode: 'YIWU', warehouseName: '义乌收货仓', warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null }, { id: 20002, tenantId: '10001', warehouseCode: 'B001', warehouseName: '交货仓 B001', warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null }, ], page: 1, size: 200, total: 2, pages: 1, }) }) it('loads the customer-scoped list and renders the first-column view action', async () => { const wrapper = mountPage() await flushPromises() const toolbar = wrapper.findComponent(QueryToolbar) const customerField = (toolbar.props('fields') as Array<{ key: string; placeholder?: string }>).find((field) => field.key === 'customerId') expect(customerField?.placeholder).toBe('全部客户') expect((toolbar.props('modelValue') as { customerId: string }).customerId).toBe('') expect(listPlatformInbounds).toHaveBeenCalledWith(undefined, expect.objectContaining({ page: 1, size: 20 })) expect(listPlatformWarehouses).not.toHaveBeenCalled() const requestsBeforeSearch = vi.mocked(listPlatformInbounds).mock.calls.length toolbar.vm.$emit('search') await flushPromises() expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeSearch + 1) expect(listPlatformInbounds).toHaveBeenLastCalledWith(undefined, expect.objectContaining({ page: 1, size: 20 })) const requestsBeforeCustomerChange = vi.mocked(listPlatformInbounds).mock.calls.length await selectCustomer(wrapper) expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeCustomerChange) expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200) toolbar.vm.$emit('search') await flushPromises() expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeCustomerChange + 1) expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ page: 1, size: 20 })) expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' }) expect(document.body.textContent).toContain('RK-001') expect(document.body.textContent).toContain('查看') expect(document.body.textContent).toContain('示例企业有限公司') expect(document.body.textContent).not.toContain('租户') expect(wrapper.find('.inbound-tabs .is-active').text()).toContain('全部') }) it('uses backend-supported warehouse IDs from tenant-scoped options without coercing Long IDs', async () => { const wrapper = mountPage() const toolbar = await selectCustomer(wrapper) toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), warehouseId: '20001', deliveryWarehouseId: '20002', }) toolbar.vm.$emit('search') await flushPromises() expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ warehouseId: '20001', deliveryWarehouseId: '20002', })) }) it('keeps common filters compact and clears hidden advanced filters when collapsed', async () => { const wrapper = mountPage() const toolbar = await selectCustomer(wrapper) const fieldKeys = () => (toolbar.props('fields') as Array<{ key: string }>).map((field) => field.key) const widths = Object.fromEntries((toolbar.props('fields') as Array<{ key: string; width?: number }>).map((field) => [field.key, field.width])) expect(widths.customerId).toBe(260) expect(widths.processStatus).toBe(160) expect(widths.inboundCode).toBe(220) expect(widths.expectedArrivalRange).toBe(320) expect(fieldKeys()).toContain('expectedArrivalRange') expect(fieldKeys()).not.toContain('feeReviewStatus') const advancedButton = () => wrapper.findAll('button').find((button) => button.text().includes('更多筛选') || button.text().includes('收起筛选')) await advancedButton()?.trigger('click') expect(fieldKeys()).toContain('feeReviewStatus') toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), feeReviewStatus: 'PENDING', destinationCode: 'ONT8', }) await flushPromises() const requestsBeforeCollapse = vi.mocked(listPlatformInbounds).mock.calls.length await advancedButton()?.trigger('click') await flushPromises() expect(fieldKeys()).not.toContain('feeReviewStatus') expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeCollapse) toolbar.vm.$emit('search') await flushPromises() expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeCollapse + 1) expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ feeReviewStatus: undefined, destinationCode: undefined, })) }) it('passes the selected backend tab and routes view to the unified order detail page', async () => { const wrapper = mountPage() const toolbar = await selectCustomer(wrapper) toolbar.vm.$emit('search') await flushPromises() const requestsBeforeTab = vi.mocked(listPlatformInbounds).mock.calls.length ;[...document.querySelectorAll('.inbound-tab')].find((button) => button.textContent?.includes('待审核'))?.click() await flushPromises() expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeTab + 1) expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ tab: 'WAIT_REVIEW', page: 1 })) ;[...document.querySelectorAll('button')].filter((button) => button.textContent?.trim() === '查看').at(-1)?.click() await flushPromises() expect(routerPush).toHaveBeenCalledWith({ name: 'unified-order-detail', params: { orderId: '18001' }, query: { tenantId: '10002' }, }) expect(wrapper.find('[aria-label="入库单详情抽屉"]').exists()).toBe(false) wrapper.unmount() }) it('downloads carton labels from the stable server-side mapping', async () => { const createObjectURL = vi.fn(() => 'blob:labels') const revokeObjectURL = vi.fn() vi.stubGlobal('URL', { createObjectURL, revokeObjectURL }) const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) const wrapper = mountPage() await flushPromises() ;[...document.querySelectorAll('button')] .filter((button) => button.textContent?.trim() === '打印箱唛').at(-1)?.click() await flushPromises() expect(downloadInboundCartonLabels).toHaveBeenCalledWith('10002', '19001') expect(createObjectURL).toHaveBeenCalledOnce() expect(revokeObjectURL).toHaveBeenCalledWith('blob:labels') expect(click).toHaveBeenCalledOnce() click.mockRestore() vi.unstubAllGlobals() }) it('does not expose row operations without the confirmed inbound view permission', async () => { useAuthStore().permissions = [] const wrapper = mountPage() await flushPromises() expect(wrapper.find('.row-actions').exists()).toBe(false) expect(wrapper.text()).toContain('无查看权限') expect(wrapper.text()).not.toContain('打印箱唛') }) it('uses the app header as the page title and exposes compact accessible status tabs', async () => { const wrapper = mountPage() await flushPromises() expect(wrapper.find('.inbound-page-heading').exists()).toBe(false) expect(wrapper.text()).not.toContain('所有处理均进入单票工作台完成') const tabList = wrapper.find('[role="tablist"]') const tabButtons = tabList.findAll('[role="tab"]') const queryToolbar = wrapper.findComponent(QueryToolbar) expect(tabList.attributes('aria-label')).toBe('入库单处理状态') expect(tabList.element.closest('.table-toolbar')).not.toBeNull() expect(queryToolbar.element.compareDocumentPosition(tabList.element) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() expect(tabButtons).toHaveLength(5) const toolbarRows = wrapper.find('.table-toolbar').element.children expect(toolbarRows[0].classList.contains('table-tabs-row')).toBe(true) expect(toolbarRows[1].classList.contains('inbound-stats-row')).toBe(true) expect(toolbarRows).toHaveLength(2) expect(wrapper.findComponent(DataTable).find('.data-table__pagination-left').text()).toContain('当前显示 1 条 · 共 1 条') const dataTableToolbarStart = wrapper.findComponent(DataTable).find('.data-table__toolbar-start') expect(dataTableToolbarStart.exists() ? dataTableToolbarStart.text() : '').not.toContain('入库单列表') expect(wrapper.text()).not.toContain('刷新') expect(wrapper.text()).not.toContain('全部入库单') expect(tabButtons[0].attributes('aria-selected')).toBe('true') expect(tabButtons[0].attributes('tabindex')).toBe('0') await tabButtons[0].trigger('keydown', { key: 'ArrowRight' }) await flushPromises() expect(tabButtons[1].attributes('aria-selected')).toBe('true') expect(listPlatformInbounds).toHaveBeenLastCalledWith(undefined, expect.objectContaining({ tab: 'WAIT_MEASURE', page: 1 })) }) it('shows an actionable list error and recovers on retry', async () => { vi.mocked(listPlatformInbounds).mockRejectedValueOnce(new Error('network down')).mockResolvedValueOnce({ records: [summary], page: 1, size: 20, total: 1, pages: 1 }) const wrapper = mountPage() await flushPromises() expect(document.body.textContent).toContain('入库单暂时无法加载') ;[...document.querySelectorAll('button')].find((button) => button.textContent?.trim() === '重新加载')?.click() await flushPromises() expect(listPlatformInbounds).toHaveBeenCalledTimes(2) expect(document.body.textContent).toContain('RK-001') }) it('updates customer-dependent options without querying until search', async () => { const wrapper = mountPage() const toolbar = await selectCustomer(wrapper) toolbar.vm.$emit('search') await flushPromises() const requestsBeforeCustomerChange = vi.mocked(listPlatformInbounds).mock.calls.length toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), customerId: '10002' }) await flushPromises() expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeCustomerChange) toolbar.vm.$emit('search') await flushPromises() expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeCustomerChange + 1) expect(listPlatformInbounds).toHaveBeenLastCalledWith('10002', expect.objectContaining({ page: 1 })) expect(listPlatformWarehouses).toHaveBeenLastCalledWith(1, 200) }) it('resets to all customers without sending an empty backend tenant ID', async () => { const wrapper = mountPage() const toolbar = await selectCustomer(wrapper) const requestsBeforeReset = vi.mocked(listPlatformInbounds).mock.calls.length const resetButton = wrapper.findAll('button').find((button) => button.text().includes('重置')) await resetButton?.trigger('click') await flushPromises() expect((toolbar.props('modelValue') as { customerId: string }).customerId).toBe('') expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeReset + 1) expect(listPlatformInbounds).toHaveBeenLastCalledWith(undefined, expect.objectContaining({ page: 1 })) expect(document.body.textContent).not.toContain('请选择所属企业') expect(document.body.textContent).not.toContain('请选择客户') }) it('loads another full-data page without a customer filter', async () => { vi.mocked(listPlatformInbounds).mockResolvedValue({ records: [summary], page: 1, size: 20, total: 60, pages: 3 }) const wrapper = mountPage() await flushPromises() const requestsBeforePage = vi.mocked(listPlatformInbounds).mock.calls.length wrapper.findComponent(DataTable).vm.$emit('update:page', 2) await flushPromises() expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforePage + 1) expect(listPlatformInbounds).toHaveBeenLastCalledWith(undefined, expect.objectContaining({ page: 2, size: 20 })) }) it('keeps the operation column first and follows the prototype field order with backend-backed columns', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/pages/InboundOrdersView.vue'), 'utf8') const toolbarSource = readFileSync(resolve(process.cwd(), 'src/shared/components/QueryToolbar.vue'), 'utf8') const columnSource = source.slice(source.indexOf('const columns:')) const operation = columnSource.indexOf("key: 'actions'") const inboundCode = columnSource.indexOf("key: 'inboundCode'") const processStatus = columnSource.indexOf("key: 'processStatus'") const orderStatus = columnSource.indexOf("key: 'orderStatus'") const measureReview = columnSource.indexOf("key: 'measureReviewStatus'") const feeStatus = columnSource.indexOf("key: 'feeStatus'") const feeReviewStatus = columnSource.indexOf("key: 'feeReviewStatus'") const customer = columnSource.indexOf("key: 'customerName'") const warehouse = columnSource.indexOf("key: 'warehouseName'") expect(operation).toBeGreaterThan(-1) expect(operation).toBeLessThan(inboundCode) expect(inboundCode).toBeLessThan(processStatus) expect(processStatus).toBeLessThan(orderStatus) expect(orderStatus).toBeLessThan(measureReview) expect(measureReview).toBeLessThan(feeStatus) expect(feeStatus).toBeLessThan(feeReviewStatus) expect(feeReviewStatus).toBeLessThan(customer) expect(customer).toBeLessThan(warehouse) expect(columnSource).toContain("fixed: 'left'") expect(columnSource).toContain("{ key: 'processStatus', label: '入库流程状态'") expect(columnSource).toContain("{ key: 'orderStatus', label: '订单状态'") expect(columnSource).toContain("{ key: 'feeStatus', label: '费用状态'") expect(columnSource).toContain("{ key: 'feeReviewStatus', label: '费用审核状态'") expect(source).toContain('