338 lines
20 KiB
TypeScript
338 lines
20 KiB
TypeScript
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, getPlatformInbound, listPlatformInbounds } from '../core/api'
|
|
import type { InboundOrderDetail, InboundOrderSummary } from '../core/types'
|
|
|
|
vi.mock('../core/api', () => ({
|
|
downloadInboundCartonLabels: vi.fn(),
|
|
getPlatformInbound: 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',
|
|
}
|
|
|
|
const detail: InboundOrderDetail = {
|
|
summary,
|
|
publicRemark: '请优先安排测量',
|
|
palletLocations: [{ allocationNo: 1, warehouseId: '20001', palletId: '21001', palletCode: 'P-001', palletCapacityCbm: 2, allocatedVolumeCbm: 1.2, allocatedPackageCount: 6, locationId: '22001', locationCode: 'A-01', reservationNo: 'RES-1', allocationStatus: 'PREPARED' }],
|
|
files: [], internalNotes: [], statusHistory: [{ fromStatus: null, toStatus: 'WAIT_MEASURE', action: 'CREATE', reason: null, operatorId: '2', operatorName: '管理员', occurredAt: '2026-08-01T10:00:00' }],
|
|
}
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
function mountPage() {
|
|
return mount(InboundOrdersView, {
|
|
attachTo: document.body,
|
|
global: { plugins: [ElementPlus] },
|
|
})
|
|
}
|
|
|
|
async function selectCustomer(wrapper: ReturnType<typeof mountPage>, 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(getPlatformInbound).mockResolvedValue(detail)
|
|
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 })
|
|
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 opens an aggregate read-only detail', 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<HTMLButtonElement>('.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<HTMLButtonElement>('button')].filter((button) => button.textContent?.trim() === '查看').at(-1)?.click()
|
|
await flushPromises()
|
|
expect(getPlatformInbound).toHaveBeenCalledWith('10002', '19001')
|
|
expect(document.body.textContent).toContain('托盘与库位')
|
|
expect(document.body.textContent).toContain('请优先安排测量')
|
|
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<HTMLButtonElement>('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<HTMLButtonElement>('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('<template #processStatus="{ row }">')
|
|
expect(source).toContain('<template #feeReviewStatus="{ row }">')
|
|
expect(source).toContain("access.can('platform:firstleg:inbound:view')")
|
|
expect(source).toContain('preference-key="inbound-orders-table"')
|
|
expect(columnSource.match(/defaultVisible: false/g)?.length).toBeGreaterThanOrEqual(25)
|
|
expect(toolbarSource).toMatch(/@media \(max-width: 720px\)[\s\S]*width: 100%/)
|
|
})
|
|
})
|