Files
admin-web/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts
T
2026-08-18 11:33:01 +08:00

355 lines
22 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { unifiedOrderStatusOptions } from '../api/unifiedOrders'
const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/pages/UnifiedOrdersView.vue'), 'utf8')
const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/routes.ts'), 'utf8')
describe('UnifiedOrdersView contract', () => {
it('only exposes the six product main statuses', () => {
expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([
'待审核', '待测量数据', '待分柜', '装柜中', '装柜完成', '派送完成',
])
for (const legacyLabel of ['待客户确认', '差异处理中', '派送中', '进入分柜']) {
expect(source).not.toContain(legacyLabel)
}
})
it('renders accessible current-status tabs with keyboard navigation', () => {
expect(source).toContain('role="tablist"')
expect(source).toContain('role="tab"')
expect(source).toContain(':aria-selected="selectedStatus === tab.value"')
expect(source).toContain("event.key === 'ArrowRight'")
expect(source).toContain("event.key === 'ArrowLeft'")
expect(source).toContain("event.key === 'Home'")
expect(source).toContain("event.key === 'End'")
})
it('provides labeled prototype-aligned advanced filters backed by real query fields', () => {
for (const key of [
'receivingWarehouseId', 'destinationWarehouseId', 'inboundCode', 'expectedArrivalDate',
'feeReviewStatus', 'containerNo', 'cabinetNo', 'channelName', 'sailingDate', 'loaded', 'fullContainer',
'measurementReviewStatus', 'fbaCode', 'createdRange',
]) expect(source).toContain(`key: '${key}'`)
for (const label of ['定位仓库', '交货仓', '费用审核状态', '是否已配载', '是否整柜', '测量数据审核状态', '商品 SKU 编码']) {
expect(source).toContain(`label: '${label}'`)
}
expect(source).toContain('listAllPlatformWarehouses')
expect(source).toContain("{ status: 'ACTIVE' }")
expect(source).toContain("warehouse.warehouseType === 'DOMESTIC'")
expect(source).toContain("loaded: optionalBoolean(activeFilters.loaded)")
expect(source).toContain("fullContainer: optionalBoolean(activeFilters.fullContainer)")
expect(source).toContain("{ value: 'UNRECORDED', label: '未录入' }")
expect(source).not.toContain("{ value: 'DRAFT', label: '草稿' }")
expect(source).not.toContain("{ value: 'CANCELLED', label: '已取消' }")
expect(source.match(/key: 'inboundCode', type: 'input'/g)).toHaveLength(1)
expect(source).not.toContain("key: 'orderNo', type:")
expect(source).not.toContain('internalOrderStatus: filters.value.internalOrderStatus')
expect(source).not.toContain("key: 'keyword'")
const prototypeFieldOrder = [
'receivingWarehouseId', 'destinationWarehouseId', 'inboundCode', 'expectedArrivalDate',
'feeReviewStatus', 'tenantId', 'containerNo', 'cabinetNo', 'channelName', 'sailingDate', 'loaded',
'fullContainer', 'measurementReviewStatus', 'fbaCode', 'createdRange',
]
const fieldPositions = prototypeFieldOrder.map((key) => source.indexOf(`key: '${key}'`))
expect(fieldPositions).toEqual([...fieldPositions].sort((left, right) => left - right))
expect(source).toContain('preference-key="unified-orders-query-v2"')
})
it('keeps unified filters aligned with the formal query contract and does not expose hold filters', () => {
const filterSource = source.slice(source.indexOf('const primaryQueryFields'), source.indexOf('const columns:'))
for (const key of ['expectedArrivalDate', 'feeReviewStatus', 'measurementReviewStatus', 'fbaCode', 'loaded', 'fullContainer']) {
expect(filterSource).toContain(`key: '${key}'`)
}
expect(filterSource).not.toContain("key: 'holdCargo'")
expect(filterSource).not.toContain("key: 'holdReason'")
expect(filterSource).toContain("key: 'fbaCode', type: 'input', width: 220, label: '商品 SKU 编码'")
expect(filterSource).not.toContain("label: '商品 FBA 编码'")
expect(source).toContain('expectedArrivalDate: activeFilters.expectedArrivalDate || undefined')
expect(source).toContain('feeReviewStatus: activeFilters.feeReviewStatus || undefined')
expect(source).toContain('measurementReviewStatus: activeFilters.measurementReviewStatus || undefined')
expect(source).toContain('fbaCode: trimmed(activeFilters.fbaCode)')
})
it('uses tabs as the only order-status filter and keeps advanced controls inside QueryToolbar', () => {
expect(source).toContain('mainStatus: selectedStatus.value || undefined')
expect(source).toContain('<template #actions>')
expect(source).toContain(':aria-expanded="showAdvancedFilters"')
expect(source).not.toContain('class="filter-mode-actions"')
expect(source).not.toContain('.filter-mode-actions')
expect(source).not.toContain('请选择订单状态')
})
it('covers real list states and keeps operation column first and left-fixed', () => {
expect(source).toContain("{ key: 'actions', label: '操作', width: 300, fixed: 'left'")
expect(source).toContain("key: 'selection', type: 'selection'")
expect(source).not.toContain('批量审核接口待接入')
expect(source).toContain('@selection-change="handleApprovalSelectionChange"')
expect(source).toContain('batchApprovePlatformUnifiedOrders')
expect(source).toContain('批量审核通过')
expect(source).toContain('title="订单列表暂时无法加载"')
expect(source).toContain('empty-text="暂无符合条件的订单"')
expect(source).toContain(':loading="loading"')
expect(source).toContain('v-model:page="page"')
expect(source).toContain('v-model:page-size="pageSize"')
const columnSource = source.slice(source.indexOf('const columns ='), source.indexOf('function emptyFilters'))
const expectedColumnKeys = [
'actions', 'orderNo', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
'destinationWarehouseName', 'channelName', 'totalPackageCount', 'totalPalletCount', 'requestedPickupAt', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg',
'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate',
'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo',
'shipmentId', 'referenceId', 'containerNo', 'cabinetNo',
'effectiveSailingAt', 'effectivePortArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
]
for (const key of expectedColumnKeys) expect(columnSource).toContain(`key: '${key}'`)
for (const uncontractedKey of [
'internalOrderStatus', 'holdCargo', 'companyName', 'pickupMethod', 'associatedFiles', 'createdBy',
]) expect(columnSource).not.toContain(`key: '${uncontractedKey}'`)
expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'orderNo'"))
expect(columnSource.indexOf("key: 'orderNo'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'"))
expect(columnSource).toContain("{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态'")
expect(columnSource).not.toContain("key: 'loadingStatus'")
expect(columnSource).not.toContain("key: 'businessDate'")
expect(columnSource).not.toContain("label: '业务日期'")
expect(columnSource).not.toContain("label: '配载状态'")
expect(columnSource).not.toContain("key: 'recommendedPalletType'")
expect(columnSource).toContain("key: 'requestedPickupAt', prop: 'requestedPickupAt', label: '预约提货时间'")
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(14)
expect(columnSource).toContain("key: 'effectiveSailingAt', prop: 'effectiveSailingAt', label: '开船时间', minWidth: 180, slotName: 'field' }")
expect(columnSource).toContain("key: 'effectivePortArrivalAt', prop: 'effectivePortArrivalAt', label: '到港时间', minWidth: 180, slotName: 'field' }")
expect(source).toContain('preference-key="unified-orders-table-v2"')
expect(source).toContain("property === 'requestedPickupAt' && (row.pickupMethod !== 'PICKUP' || !value)")
expect(source).toContain("property === 'skuNos' ? value.join('、')")
expect(source).not.toContain('<dt>推荐托盘类型</dt>')
expect(source).toContain("estimated === true ? '预计 ' : ''")
expect(source).toContain('row.transport?.effectiveSailingAtEstimated')
expect(source).toContain('row.transport?.effectivePortArrivalAtEstimated')
expect(source).not.toContain("transport.sailingAt")
expect(source).not.toContain("transport.portArrivalAt")
expect(source).toContain('.table-card { overflow: hidden; }')
expect(source).toContain('slotName: \'measurementReviewStatus\'')
expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')')
expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '客户备注'")
expect(source).toContain("{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '内部备注'")
expect(source.match(/<LineClampText v-if="column\.key === 'remark'"/g)).toHaveLength(3)
})
it('uses ListPageLayout regions for one 16px query-to-table gap', () => {
expect(source).toContain('<ListPageLayout ariaLabel="统一订单列表">')
expect(source).toContain('<template #toolbar>')
expect(source).toContain('<template v-if="loadError" #state>')
expect(source).toContain('<template #default>')
expect(source).toContain('v-if="!loadError" id="unified-orders-results" class="surface-panel table-panel table-card"')
expect(source).not.toContain('class="orders-load-error"')
expect(source).not.toContain('.orders-load-error { margin-top: 0; }')
})
it('counts detail in the three directly visible row actions', () => {
expect(source).toContain('unifiedOrderSecondaryActions(row).slice(0, 2)')
expect(source).toContain('unifiedOrderSecondaryActions(row).slice(2)')
expect(source).toContain('<Eye :size="15" />详情')
expect(source).toContain('v-if="moreUnifiedOrderActions(row as UnifiedOrderResource).length"')
})
it('opens smart loading from the order table toolbar with its own permission', () => {
expect(source).toContain("can('platform:firstleg:smart-loading:list')")
expect(source).toContain('#toolbar-actions')
expect(source).toContain('@click="smartLoadingOpen = true"')
expect(source).toContain('<SmartLoadingDialog v-model="smartLoadingOpen" />')
})
it('keeps editable query values separate from the applied list request', () => {
expect(source).toContain('const appliedFilters = ref<FilterModel>(emptyFilters())')
expect(source).toContain('const activeFilters = appliedFilters.value')
expect(source).toContain('appliedFilters.value = cloneFilters(filters.value)')
expect(source).toContain('appliedFilters.value = emptyFilters()')
expect(source).not.toContain('filters.value.mainStatus')
})
it('keeps only confirmed business actions as explicit boundaries', () => {
expect(source).toContain('nextActionBoundary')
expect(source).not.toContain('class="action-boundary"')
expect(source).toContain("can('platform:firstleg:inbound:measure:view')")
expect(source).toContain('openMeasurement')
expect(source).toContain('<InboundMeasurementDialog')
expect(source).toContain('measurementDialogOpen.value = true')
expect(source).toContain('@updated="handleMeasurementUpdated"')
expect(source).toContain('@submitted="handleMeasurementSubmitted"')
expect(source).not.toContain('<InboundMeasurementPanel')
expect(source).not.toContain('class="inbound-task-dialog"')
expect(source).not.toContain("name: 'inbound-order-detail'")
expect(source).not.toContain('openInboundFromDetail')
expect(source).not.toContain('inbound-code-link')
expect(source).not.toContain('待接入:智能分柜')
expect(source).not.toContain('updateMainStatus')
expect(source).not.toContain('人工进入分柜')
expect(source).not.toContain('performPlatformUnifiedOrderAction')
expect(source).not.toContain('/actions/')
})
it('treats WAIT_SPLIT as a display/filter state without inventing a split action', () => {
expect(source).toContain('...unifiedOrderStatusOptions')
expect(source).toContain('function nextActionBoundary(row: UnifiedOrderResource)')
expect(source).toContain("return row.nextAction === 'CONFIRM_MEASUREMENT' ? '待完成测量' : ''")
expect(source).not.toContain('人工进入分柜')
expect(source).not.toContain("action === 'split'")
expect(source).not.toContain("nextAction === 'SMART_SPLIT'")
})
it('approves pending review orders without exposing an order carrier assignment', () => {
expect(source).toContain("row.nextAction === 'APPROVE_ORDER'")
expect(source).toContain("can('platform:order:approve')")
expect(source).toContain('await approvePlatformUnifiedOrder(tenantId, orderId)')
expect(source).toContain('title="审核订单"')
expect(source).toContain('confirm-text="审核通过"')
expect(source).not.toContain('assignPlatformUnifiedOrderCarrier')
expect(source).not.toContain('listPlatformPartners')
expect(source).not.toContain('指派承运商')
expect(source).not.toContain('待平台指派')
})
it('provides unified detail, history and internal note states', () => {
expect(source).toContain('getPlatformUnifiedOrder')
expect(source).toContain('aria-label="状态历史"')
expect(source).toContain('aria-label="内部备注"')
expect(source).toContain('订单详情暂时无法加载')
expect(source).toContain('一、客户下单数据')
expect(source).toContain('二、外箱规数据')
expect(source).toContain('三、实际测量数据')
expect(source).toContain('measurementCollapsed = reactive({ customer: false, outer: false, actual: false })')
expect(source).toContain('function toggleMeasurementSection(section: keyof typeof measurementCollapsed)')
expect(source).toContain(':aria-expanded="!measurementCollapsed.customer"')
expect(source).toContain(':aria-expanded="!measurementCollapsed.outer"')
expect(source).toContain(':aria-expanded="!measurementCollapsed.actual"')
expect(source).toContain('class="measurement-section-summary"')
expect(source).toContain('<ChevronUp v-if="!measurementCollapsed.customer"')
expect(source).toContain('<ChevronDown v-else :size="18" />')
expect(source).toContain("{ key: 'girthCm', label: '箱规周长(cm)' }")
expect(source).toContain('runMoreAction($event, row as UnifiedOrderResource)')
expect(source).toContain(':aria-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
expect(source).toContain(':title="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
expect(source).toContain("can('platform:order:note')")
expect(source).toContain('await loadOrders()')
expect(source).toContain('orderStatusLabel(detail.summary.internalOrderStatus || detail.summary.mainStatus)')
expect(source).toContain('feeReviewLabel(detail.summary.feeReviewStatus)')
expect(source).toContain('unifiedOrderLoadingStatusLabel(detail.summary.transport?.loadingStatus)')
expect(source).not.toContain("value.replaceAll('_', ' ')")
})
it('binds dimension and weight evidence to their formal server records without mixing galleries', () => {
expect(source).toContain('<el-table :data="measurementDetail?.items || []" row-key="id"')
expect(source).toContain('label="尺寸图片"')
expect(source).toContain(':file-id="row.intelligentImageFileId"')
expect(source).toContain('openMeasurementEvidence(row.intelligentImageFileId')
expect(source).toContain('v-for="weightResult in measurementDetail.weightResults"')
expect(source).toContain(':file-id="weightResult.fileId"')
expect(source).toContain('openMeasurementEvidence(weightResult.fileId')
expect(source).toContain('measurementDetail?.actualGrossWeightKg')
expect(source).toContain('weightResult.grossWeightKg')
expect(source).toContain('weightResult.netWeightKg')
expect(source).toContain("weightResult.palletType || '未记录类型'")
expect(source).not.toContain('weightResult.palletCode')
expect(source).not.toContain('measurementImageUrls')
expect(source).not.toContain('const fileIds = [...measurementDetail.value.items')
})
it('keeps authenticated evidence loading, original preview, permission and error states', () => {
expect(source).toContain('<MeasurementEvidenceThumbnail')
expect(source).toContain("downloadWeightImage(String(summary.tenantId), String(summary.inboundId), fileId)")
expect(source).toContain('measurementPreviewError')
expect(source).toContain('测量原图暂时无法读取')
expect(source).toContain("can('platform:firstleg:inbound:measure:view')")
})
it('shows parent-child markers only for related rows and keeps the aggregate in detail', () => {
expect(source).not.toContain("type: 'expand'")
expect(source).toContain('v-if="(row as UnifiedOrderResource).relation"')
expect(source).toContain('父单 · ${row.relation.childCount} 个子单')
expect(source).toContain('子单 · 第 ${row.relation.splitSequence} 次')
expect(source).toContain('title="父子订单关联"')
expect(source).toContain('@click="openOrderRelation(row as UnifiedOrderResource)"')
expect(source).toContain("{ key: 'fulfillment', label: '拆分履约', icon: OrderFulfillmentIcon }")
expect(source).toContain("activeDetailTab === 'fulfillment'")
expect(source).toContain('根单、已完成分片与当前余量')
})
it('distinguishes overflow action buttons by row and keeps a stable missing-field fallback', () => {
expect(source).toContain('function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource)')
expect(source).toContain("String(row.inboundCode || '').trim() || '当前入仓单'")
expect(source).not.toContain('compatibleOrder.orderNo')
expect(source).not.toContain('aria-label="更多订单操作"')
expect(source).not.toContain('title="更多操作"')
})
it('renders order detail as a routed full page instead of a drawer', () => {
expect(source).toContain('<template v-if="orderId">')
expect(source).toContain('class="order-detail-header"')
expect(source).toContain('aria-label="订单详情模块"')
expect(source).toContain("{ key: 'basic', label: '基本信息'")
expect(source).toContain("{ key: 'measurement', label: '测量数据'")
expect(source).not.toContain('detailOpen')
expect(source).toContain('watch(detailRouteKey,')
expect(source).toContain('requestKey !== detailRouteKey.value')
})
it('keeps the basic information compact and maps available order fields', () => {
expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
for (const label of ['业务类型', '买单 / 退税', '提货方式', '配载状态', '目的仓类型', '总净重', '商检验', '提货联系人', '目的地地址']) {
expect(source).toContain(`<dt>${label}</dt>`)
}
expect(source).toContain("partyValue(detail.order.parties, 'SENDER', 'phone')")
expect(source).toContain("partyValue(detail.order.parties, 'RECEIVER', 'phone')")
expect(source).not.toContain("partyValue(detail.order.parties, 'CONSIGNEE'")
expect(source).toContain("cargoTotal(detail.order.cargoes, 'netWeightKg')")
expect(source).toContain('InboundFeePanel')
expect(source).toContain('InboundFeeDetailManager')
expect(source).toContain(':customer-id="String(detail.summary.tenantId)"')
expect(source).toContain('compact')
expect(source).toContain('头程自动初算')
expect(source).toContain('其他费用明细')
expect(source).toContain('@changed="loadDetail(true)"')
expect(source).toContain('firstLegTransportModeLabel(detail.order.order?.transportMode)')
expect(source).toContain('firstLegPickupMethodLabel(detail.order.order?.pickupMethod)')
expect(source).toContain('firstLegDestinationTypeLabel(detail.order.order?.destinationType)')
})
it('mounts downstream progress inside the existing order detail without opening unsafe writes', () => {
expect(source).toContain('UnifiedOrderWorkflowPanel')
expect(source).toContain("{ key: 'workflow', label: '后半程作业', icon: OrderFulfillmentIcon }")
expect(source).toContain(':order-id="String(detail.summary.orderId)"')
expect(source).not.toContain(':show-delivery=')
expect(source).toContain("detail.value?.summary.inboundId")
expect(routeSource).not.toContain('unified-order-workflow')
})
it('keeps detail tabs compact on one horizontally scrollable row', () => {
expect(source).toContain('.detail-tabs { display: flex; overflow-x: auto; overflow-y: hidden;')
expect(source).toContain('min-width: max-content; min-height: 56px; flex: 0 0 auto;')
expect(source).not.toContain('grid-template-columns: repeat(6, minmax(124px, 1fr))')
expect(source).not.toContain('.detail-tabs { grid-template-columns: repeat(6, 132px); }')
})
it('matches the prototype table grid and distinguishes container identifiers', () => {
expect(source).toContain('row-key="orderId" border stripe')
expect(source).toContain('<el-table :data="detail.order.cargoes" border stripe')
expect(source.match(/<el-table :data="measurementDetail\?\.items \|\| \[\]"[^>]*border stripe/g)).toHaveLength(2)
expect(source).toContain('<el-table :data="detail.order.cargoes" border stripe empty-text="暂无客户下单数据"')
expect(source).toContain('--el-table-border-color: #e5e7eb')
expect(source).toContain('--el-table-row-hover-bg-color: #f3f7fb')
expect(source).toContain('.el-table--striped .el-table__body tr.el-table__row--striped')
expect(source).toContain("label: '真实集装箱号'")
expect(source).toContain("label: '内部柜次号'")
expect(source).toContain('<dt>真实集装箱号</dt>')
expect(source).toContain('<dt>内部柜次号</dt>')
})
it('uses outlined semantic buttons for row business actions while keeping detail navigation explicit', () => {
const rowActions = source.slice(source.indexOf('<template #actions'), source.indexOf('<template #inboundCode'))
expect(rowActions).toContain('size="small" plain :type="unifiedOrderActionType(action)"')
expect(rowActions).not.toContain('v-for="action in directUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" link')
expect(rowActions).toContain('<Eye :size="15" />详情')
expect(source).toContain('<el-button link class="back-button"')
})
})