Files
admin-web/src/modules/loading-lists/components/LoadingListsView.test.ts
T
2026-09-30 14:01:58 +08:00

262 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const source = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListsView.vue'), 'utf8')
const detailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/components/LoadingListDetailDrawer.vue'), 'utf8')
const preloadDetailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/pages/PreloadPlanDetailView.vue'), 'utf8')
const confirmedDetailSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/pages/ConfirmedLoadingListDetailView.vue'), 'utf8')
const routesSource = readFileSync(resolve(process.cwd(), 'src/modules/loading-lists/routes.ts'), 'utf8')
describe('LoadingListsView', () => {
it('filters both list modes by an active name-only customer selector', () => {
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
expect(source).toContain("label: customer.tenantName")
expect(source).toContain("customerId: String(query.customerId || '').trim() || undefined")
expect(source).not.toContain('customer.tenantCode')
})
it('keeps the operation column first and fixed left in both lists', () => {
const actionColumns = [...source.matchAll(/key: 'actions', label: '操作', fixed: 'left'/g)]
expect(actionColumns).toHaveLength(2)
expect(source.indexOf("key: 'actions'")).toBeLessThan(source.indexOf("key: 'preloadNo'"))
expect(source.indexOf("key: 'actions'", source.indexOf("key: 'actions'") + 1)).toBeLessThan(source.indexOf("key: 'loadingListNo'"))
})
it('queries all warehouses without exposing a warehouse selector', () => {
expect(source).not.toContain('listAllPlatformWarehouses')
expect(source).not.toContain("key: 'warehouseId'")
expect(source).not.toContain('loadingWarehouses')
expect(source).not.toContain('warehouseId: warehouseId || undefined')
expect(source).toContain('query: { warehouseId: String(row.warehouseId || \'\') }')
expect(source).not.toContain("ElMessage.warning('请先选择仓库')")
expect(source).not.toContain("'请先选择仓库后查询'")
expect(preloadDetailSource).toContain('MISSING_WAREHOUSE_CONTEXT')
expect(confirmedDetailSource).toContain('MISSING_WAREHOUSE_CONTEXT')
})
it('uses the shared query and table components while keeping details on route pages', () => {
expect(source).toContain('QueryToolbar')
expect(source).toContain('DataTable')
expect(source).toContain('LoadErrorState')
expect(source).toContain('ApiErrorMeta')
expect(preloadDetailSource).toContain('AppDialog')
expect(confirmedDetailSource).not.toContain('AppDialog')
expect(detailSource).not.toContain('AppDrawer')
expect(detailSource).not.toContain('ContainerFeesPanel')
})
it('distinguishes the generated cabinet reference from the physical container number', () => {
expect(source).toContain("label: '柜次号'")
expect(source).toContain("label: '集装箱号'")
expect(source).toContain('装柜清单号/柜次号/集装箱号')
expect(detailSource).toContain('<span>柜次号</span>')
expect(detailSource).toContain('<span>集装箱号</span>')
expect(source).not.toContain("label: '柜号'")
expect(detailSource).not.toContain('<span>柜号</span>')
expect(source).toContain("key: 'cabinetNo', prop: 'cabinetNo'")
expect(source).not.toContain('internalContainerNo')
})
it('keeps preload routes while completed lists move into domestic container detail', () => {
expect(source).toContain("name: 'preload-plan-detail'")
expect(source).toContain("name: 'loading-list-detail'")
expect(source).not.toContain('confirmed-inline-detail')
expect(preloadDetailSource).toContain('返回列表')
expect(preloadDetailSource).toContain('router.back()')
expect(confirmedDetailSource).toContain('返回列表')
expect(confirmedDetailSource).toContain('router.back()')
expect(confirmedDetailSource).toContain('@media (min-width: 1920px) { .confirmed-loading-detail-page { width: 100%; max-width: none; gap: 12px; } }')
expect(routesSource).not.toContain("path: 'loading-lists/:planId'")
expect(routesSource).not.toContain("name: 'loading-list-detail'")
})
it('keeps the real transport edit contract in a 620px centered modal on the detail page', () => {
expect(source).not.toContain('class="loading-transport-dialog"')
expect(preloadDetailSource).toContain('width="min(620px, calc(100vw - 28px))"')
expect(preloadDetailSource).not.toContain('备注信息(仅自己可见)')
expect(preloadDetailSource).not.toContain('备注信息(客户可见)')
expect(preloadDetailSource).not.toContain('备注信息(仓库可见)')
})
it('only exposes transport editing for LOADING preload details', () => {
expect(detailSource).toContain("props.mode === 'preload' && props.canEdit && ['EDITING', 'LOADING'].includes(summary.value?.status || '')")
expect(detailSource).toContain('const itemColumns = computed(() => canEditItemRemark.value ? [actionColumn, ...baseItemColumns] : baseItemColumns)')
expect(detailSource).not.toContain('报关资料')
expect(detailSource).not.toContain('完成报关')
expect(preloadDetailSource).toContain('expectedVersion: detail.value.summary.version')
})
it('renders business labels instead of raw loading-plan status codes', () => {
expect(source).toContain(':status="loadingListStatusLabel(row.status)"')
expect(detailSource).toContain(':status="loadingListStatusLabel(summary.status)"')
expect(detailSource).not.toContain(':status="summary.status"')
expect(source).not.toContain('仅装柜中允许修改')
})
it('keeps transport editing as a detail-page action', () => {
expect(source).not.toContain('openEditFromRow(row)')
expect(source).not.toContain('编辑运输信息')
expect(detailSource).toContain("summary.containerNo || '待补录'")
expect(detailSource).toContain('v-if="canEditTransport"')
expect(preloadDetailSource).toContain("isContainerCorrection.value ? '纠正集装箱号 / 航次' : '补录集装箱号 / 航次'")
})
it('refreshes the detail version on A0409 while preserving transport input', () => {
expect(preloadDetailSource).toContain("details.code === 'A0409'")
expect(preloadDetailSource).toContain('await loadDetail()')
expect(preloadDetailSource).toContain('当前输入已保留,请核对后重新保存')
})
it('opens a validated plan and warehouse deep link without mixing list modes', () => {
expect(source).toContain('useRoute()')
expect(source).toContain('route.query.planId')
expect(source).toContain('route.query.warehouseId')
expect(source).toContain("router.replace({ name: 'loading-list-detail'")
expect(confirmedDetailSource).toContain("result.summary.status !== 'COMPLETED'")
expect(confirmedDetailSource).toContain('MODE_STATUS_MISMATCH')
expect(confirmedDetailSource).toContain('watch([planId, warehouseId], () => void loadDetail())')
expect(confirmedDetailSource).toContain('if (sequence !== loadSequence) return')
expect(preloadDetailSource).toContain('watch([planId, warehouseId], () => {')
})
it('uses backend readiness for whole-plan confirmation and translates blocking reasons', () => {
expect(detailSource).toContain(':disabled="!preloadSummary?.readiness.canConfirm || !pdaAssigneeSelected"')
expect(detailSource).toContain("OCEAN_BOOKING_NOT_RELEASED: '关联订舱尚未放舱'")
expect(detailSource).toContain("PRELOAD_ALREADY_CONFIRMED: '整份预装计划已经确认'")
expect(detailSource).toContain("confirmationStatus !== 'CONFIRMED' && !preloadSummary.readiness.canConfirm")
expect(detailSource).toContain('confirmationStatusLabels[preloadSummary.readiness.confirmationStatus]')
expect(detailSource).toContain("pdaTaskStatusLabels")
expect(preloadDetailSource).toContain('@confirm-preload="openConfirmPreload"')
})
it('passes translated status text directly to the badge', () => {
expect(source).toContain('<StatusBadge :status="loadingListStatusLabel(row.status)" />')
expect(source).not.toContain('<StatusBadge :status="row.status" />')
expect(detailSource).toContain('<StatusBadge :status="loadingListStatusLabel(summary.status)" />')
})
it('uses text row actions in the integrated list while retaining detail table styles', () => {
expect(source).toContain(":class=\"{ 'density-action': isPreload }\"")
expect(source).toContain(':type="isPreload ? undefined : \'primary\'" :plain="!isPreload"')
expect(source).not.toContain('<el-button v-if="canView" link')
expect(source).toContain("key: 'actions', label: '操作', fixed: 'left', width: 110")
expect(source).toContain('flex-wrap: wrap')
expect(source).toContain('@media (max-width: 720px)')
expect(source).toContain('.row-actions { gap: 4px; }')
expect(source).toContain('row-actions density-actions admin-text-actions')
expect(source).toContain('appearance="integrated" :stripe="false"')
expect(detailSource).toMatch(/:columns="itemColumns"\s+stripe/)
})
it('shows every prototype detail field that is backed by the loading item contract', () => {
expect(detailSource.indexOf("key: 'actions'")).toBeLessThan(detailSource.indexOf("key: 'sequence'"))
expect(detailSource).toContain("key: 'actions', label: '操作', fixed: 'left'")
expect(detailSource.indexOf("key: 'orderNo'")).toBeLessThan(detailSource.indexOf("key: 'containerNo'"))
expect(detailSource.indexOf("key: 'containerNo'")).toBeLessThan(detailSource.indexOf("key: 'skuCode'"))
const fields = [
'taxRefundFlag',
'chineseName',
'englishName',
'netWeightKg',
'packageCount',
'palletCount',
'volumeCbm',
'grossWeightKg',
'dimensionalWeightKg',
'productQuantity',
'unitsPerPackage',
'fbaTrackingNo',
'shipmentNo',
'shippingMark',
'customerRemark',
'internalRemark',
'warehouseRemark',
'customerServiceRemark',
]
let previous = detailSource.indexOf("key: 'customerName'")
for (const field of fields) {
const current = detailSource.indexOf(`key: '${field}'`, previous + 1)
expect(current, field).toBeGreaterThan(previous)
previous = current
}
expect(detailSource).not.toContain("key: 'goodsImageUrl'")
expect(detailSource).not.toContain("label: '退税资料'")
})
it('keeps customer remarks read-only while exposing three editable collaboration remarks', () => {
expect(detailSource).toContain('来源:客户下单')
expect(detailSource).toContain('来源:订单内部备注')
expect(detailSource).not.toContain('仓库备注将随装柜完成资料下发海外仓')
expect(detailSource).toContain("emit('editRemarks', row as LoadingItem)")
expect(detailSource).toContain("props.mode === 'preload' && props.canEdit && ['EDITING', 'LOADING'].includes(summary.value?.status || '')")
expect(detailSource).not.toContain("emit('editCustomerRemark'")
expect(detailSource).toContain("key: 'customerServiceRemark'")
})
it('uses one white detail surface, a readable four-column summary and dense item table', () => {
expect(detailSource).toContain('class="detail-overview"')
expect(detailSource).not.toContain('class="detail-overview surface-panel"')
expect(detailSource).toContain('class="detail-page-header"')
expect(detailSource).toContain("@click=\"emit('back')\"")
expect(detailSource).toContain('class="detail-page-header__actions"')
expect(detailSource).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
expect(detailSource).toContain('min-height: 26px')
expect(detailSource).toContain('> div > span:first-child::after { content: ":"; }')
expect(detailSource).not.toContain('> div > span::after { content: ":"; }')
expect(detailSource).toContain('background: var(--admin-surface, #fff)')
expect(detailSource).toContain('overflow-wrap: anywhere')
expect(detailSource).toContain("className: 'identifier-column'")
expect(detailSource).toContain('showOverflowTooltip: true')
expect(detailSource).toContain('white-space: nowrap')
expect(detailSource).toContain('<template #toolbar-start>')
expect(detailSource).not.toContain('<span v-else aria-label="只读">—</span>')
})
it('merges the page header and summary while spacing independent business sections', () => {
expect(detailSource).toContain('<section v-if="detail && summary" class="detail-summary"')
expect(detailSource).not.toContain('class="detail-summary surface-panel"')
expect(detailSource).toContain('.detail-layout :deep(.detail-page-layout__content) { display: grid; min-width: 0; gap: 20px; }')
expect(detailSource).toContain('<section class="detail-section">')
expect(detailSource).not.toContain('class="detail-section surface-panel"')
expect(detailSource).not.toContain('class="history-section surface-panel"')
expect(detailSource).toContain('padding: var(--admin-space-6, 16px) 0 0')
expect(detailSource).toContain("fixed: 'left', width: 132")
})
it('does not expose unknown history action codes', () => {
expect(detailSource).toContain("CONFIRM_PRELOAD: '确认预装'")
expect(detailSource).toContain("[action] || '状态更新'")
expect(detailSource).not.toContain('[action] || action')
})
it('uses safe labels when customer or operator names are missing', () => {
expect(source).toContain("item.customerName || '客户信息缺失'")
expect(source).not.toContain('item.customerName || item.accountId')
expect(detailSource).toContain("item.customerName || '客户信息缺失'")
expect(detailSource).toContain("row.operatorName || '操作人信息缺失'")
expect(detailSource).not.toContain('item.customerName || item.accountId')
expect(detailSource).not.toContain('row.operatorName || row.operatorId')
})
it('does not retain the retired loading-list customs entry', () => {
expect(confirmedDetailSource).not.toContain('LoadingDeclaration')
expect(confirmedDetailSource).not.toContain('customsDocument')
expect(detailSource).not.toContain('customsDocument')
expect(detailSource).not.toContain('报关资料')
})
it('filters pure status no-ops and counts identity-distinct visible history events', () => {
for (const marker of ['visibleHistories', 'STATUS_UPDATE', 'operationId', 'PUSH_PRELOAD']) expect(detailSource).toContain(marker)
expect(detailSource).toContain('visibleHistories.length }} 条')
expect(detailSource).toContain(':data="visibleHistories"')
expect(detailSource).not.toContain('detail.histories.length }} 条')
expect(detailSource).not.toContain('v-for="history in detail.histories"')
expect(detailSource).toContain('seen.has(eventKey)')
expect(detailSource).toContain('history.operationId')
const sameWording = [
{ historyId: '4', operationId: '', actionType: 'ADJUST_ITEM', operationReason: '相同文案' },
{ historyId: '5', operationId: '', actionType: 'ADJUST_ITEM', operationReason: '相同文案' },
]
expect(new Set(sameWording.map((history) => history.historyId)).size).toBe(2)
})
})