Files
admin-web/src/modules/inbounds/components/InboundTaskList.test.ts
T
2026-10-20 23:34:11 +08:00

211 lines
12 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import type { PlatformTenantResource } from '@/modules/iam'
import { loadAllActiveCustomers } from '../task-list'
function activeCustomer(id: string, tenantName: string, legalName: string | null, tenantCode: string): PlatformTenantResource {
return {
id, tenantName, legalName, tenantCode, status: 'ACTIVE', 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,
}
}
describe('InboundTaskList measurement scope', () => {
it('uses supplied order status tabs in the toolbar and keeps standalone mode titles as fallback', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
const toolbar = source.match(/<template #toolbar-start>([\s\S]*?)<\/template>/)?.[1]
const filters = source.match(/<template #filters>([\s\S]*?)<\/template>/)?.[1]
expect(toolbar).toContain('<slot name="status-tabs"><h1>')
expect(toolbar).toContain("title || (isReview ? '测量数据审核' : '订单测量')")
expect(toolbar).toContain('</h1></slot>')
expect(filters).toContain('<QueryToolbar')
expect(filters).not.toContain('name="status-tabs"')
expect(source).toContain('<template v-if="isReview && access.can(actionPermission)" #toolbar-actions>')
})
it('supports selecting only pending measurement reviews for batch approval', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).toContain("key: 'selection', type: 'selection'")
expect(source).toContain("row.measureReviewStatus === 'PENDING'")
expect(source).toContain('@selection-change="handleReviewSelectionChange"')
expect(source).toContain('batchApproveMeasurements')
expect(source).toContain('批量审核通过')
expect(source).toContain('getMeasurementReviewDetail')
expect(source).toContain('approveMeasurement')
expect(source).toContain('expectedVersion: detail.inboundVersion')
expect(source).not.toContain('expectedVersion: detail.version')
expect(source).toContain('approval.initialPricingSucceeded')
expect(source).toContain('费用初算未生成,可补价后重新计算')
})
it('uses the inbound tenant isolation key for measurement and review requests', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).toContain(':tenant-id="selectedInbound.tenantId"')
expect(source).toContain(':customer-id="selectedInbound.tenantId"')
expect(source).toContain(':customer-name="customerDisplayName(selectedInbound)"')
expect(source).toContain(':channel-name="selectedInbound.channelName"')
expect(source).not.toContain('selectedInbound.customerId || selectedInbound.tenantId')
})
it('opens the unique shared measurement dialog and refreshes without duplicating its workflow', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).toContain("import InboundMeasurementDialog from '../measurement/InboundMeasurementDialog.vue'")
expect(source).toContain('<InboundMeasurementDialog')
expect(source).toContain('v-model="measurementDialogOpen"')
expect(source).toContain(':customer-id="selectedInbound.tenantId"')
expect(source).toContain('@updated="handleMeasurementUpdated"')
expect(source).toContain('@submitted="handleMeasurementSubmitted"')
expect(source).not.toContain('<WeightOcrPanel')
expect(source).not.toContain('<InboundMeasurementPanel')
expect(source).toContain("import AppDialog from '@/shared/components/AppDialog.vue'")
expect(source).toContain(':show-actions="false"')
expect(source).toContain('<template #footer="{ close }">')
expect(source).toContain('reviewPanelRef?.confirmApprove()')
})
it('loads only active customers and exposes a searchable name-only select', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
const queryFieldsSource = source.slice(source.indexOf('const queryFields'), source.indexOf('const columns'))
expect(source).toContain('loadAllActiveCustomers')
expect(queryFieldsSource).toContain("type: 'select'")
expect(queryFieldsSource).toContain('filterable: true')
expect(queryFieldsSource).toContain('label: customer.legalName?.trim() || customer.tenantName')
expect(queryFieldsSource).not.toContain('tenantCode')
expect(queryFieldsSource).not.toContain('客户编码')
})
it('clears date conditions safely and uses explicit select placeholders', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
const queryFieldsSource = source.slice(source.indexOf('const queryFields'), source.indexOf('const columns'))
expect(source).toContain('Array.isArray(filters.value.expectedArrivalRange)')
expect(queryFieldsSource).toContain("placeholder: customerLoadError.value ? '客户列表加载失败' : '请选择客户'")
expect(queryFieldsSource).toContain("placeholder: '请选择仓库'")
})
it('requests every active-customer page and merges name-only options', async () => {
const calls: Array<{ page: number; size: number; status: 'ACTIVE' }> = []
const records = await loadAllActiveCustomers(async (query) => {
calls.push(query)
if (query.page === 1) {
return {
pages: 2,
records: [activeCustomer('10001', '客户一', '客户一有限公司', 'CUS-001')],
}
}
return {
pages: 2,
records: [activeCustomer('10002', '客户二', null, 'CUS-002')],
}
})
expect(calls).toEqual([
{ page: 1, size: 200, status: 'ACTIVE' },
{ page: 2, size: 200, status: 'ACTIVE' },
])
expect(records.map((customer) => customer.legalName?.trim() || customer.tenantName)).toEqual(['客户一有限公司', '客户二'])
expect(records.map((customer) => customer.legalName?.trim() || customer.tenantName).join(' ')).not.toContain('CUS-')
})
it('opens the shared unified order detail from the view action', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).toContain("name: 'unified-order-detail', params: { orderId: row.orderId }, query: { tenantId: row.tenantId }")
expect(source).toContain("access.can('platform:firstleg:inbound:view') && (row as InboundOrderSummary).orderId")
expect(source).not.toContain("name: 'inbound-order-detail', params: { inboundId: row.id }")
})
it('keeps relationship data entry out of measurement and review task lists', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).not.toContain('关联数据')
expect(source).not.toContain('InboundCodeRelationshipDialog')
expect(source).not.toContain('openRelationship')
})
it('closes only after navigating an approved review to the measurement tab', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
const handler = source.slice(source.indexOf('async function handleTaskChanged'), source.indexOf('function handleReviewSelectionChange'))
expect(handler).toContain("name: 'unified-order-detail-by-inbound-code'")
expect(handler).toContain('params: { inboundCode }')
expect(handler).toContain("query: { tab: 'measurement', from: 'measurement-review' }")
expect(handler).toContain('await router.push')
expect(handler.indexOf('await router.push')).toBeLessThan(handler.lastIndexOf('reviewDialogOpen.value = false'))
expect(handler).toContain("getApiErrorDetails(error, '订单详情打开失败,请重试')")
expect(handler).toContain("name: 'unified-order-detail'")
expect(handler).toContain("query: { tenantId: target.tenantId, tab: 'measurement', from: 'measurement-review' }")
expect(source).toContain(':post-approve-loading="reviewNavigationLoading"')
expect(source).toContain(':approval-completed="reviewApproved"')
expect(source).toContain(':close-on-click-modal="!reviewNavigationLoading"')
expect(source).toContain('reviewNavigationError')
expect(source).toContain('重新打开订单详情')
expect(source).not.toContain('params: { inboundId: target.id }')
})
it('shows authoritative measurement totals with operation first and no internal IDs as primary columns', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
const columnsSource = source.slice(source.indexOf('const columns'), source.indexOf('function text'))
expect(columnsSource.indexOf("key: 'actions'")).toBeLessThan(columnsSource.indexOf("key: 'inboundCode'"))
expect(columnsSource).toContain("key: 'customerName', label: '客户'")
expect(columnsSource).toContain("key: 'channelName', label: '渠道'")
expect(columnsSource).toContain("key: 'totalPackageCount', label: '总箱数'")
expect(columnsSource).toContain("key: 'palletCount', label: '托盘数'")
expect(columnsSource).toContain("key: 'actualGrossWeightKg', label: '实测毛重(kg)'")
expect(columnsSource).toContain("key: 'actualNetWeightKg', label: '货物重量(kg)'")
expect(columnsSource).toContain("key: 'actualVolumeCbm', label: '实测体积(m³)'")
expect(columnsSource).toContain("key: 'measureCompletedAt', label: '提交时间'")
expect(columnsSource).not.toContain("key: 'id'")
expect(columnsSource).not.toContain("key: 'orderId'")
expect(source).toContain('{{ formatNumber((row as InboundOrderSummary).actualGrossWeightKg) }}')
expect(source).toContain('{{ formatNumber((row as InboundOrderSummary).actualNetWeightKg) }}')
expect(source).not.toContain('actualGrossWeightKg ||')
expect(source).not.toContain('actualNetWeightKg ||')
})
it('renders missing list facts as an explicit dash instead of guessing values', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).toContain("value === null || value === undefined || value === '' ? '—'")
expect(source).toContain("value === null || value === undefined ? '—'")
expect(source).toContain("if (!value) return '—'")
})
it('shows the expected arrival as a date in both measurement and review lists', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).toContain("date.toLocaleDateString('zh-CN')")
expect(source).toContain('{{ formatDateOnly((row as InboundOrderSummary).expectedArrivalAt) }}')
expect(source).toContain('{{ formatDate((row as InboundOrderSummary).measureCompletedAt) }}')
expect(source).not.toContain('{{ formatDate((row as InboundOrderSummary).expectedArrivalAt) }}')
})
it('renders first-leg business types with the shared Chinese labels', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).toContain("import { firstLegBusinessTypeLabel } from '@/shared/utils/firstLegBusinessLabels'")
expect(source).toContain('firstLegBusinessTypeLabel((row as InboundOrderSummary).businessType)')
expect(source).not.toContain('text((row as InboundOrderSummary).businessType)')
})
it('refreshes cached task lists when users return from measurement work', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
expect(source).toContain('onActivated(() => {')
expect(source).toContain('if (!activatedOnce)')
expect(source).toContain('activatedOnce = true')
expect(source).toContain('void loadTasks()')
expect(source).toContain('onMounted(() => { void loadOptions(); void loadTasks() })')
})
})