2026-10-20 10:11:31 +08:00
import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
import { flushPromises , mount , type VueWrapper } from '@vue/test-utils'
import { createRequire } from 'node:module'
import DomesticContainersView from './DomesticContainersView.vue'
import * as api from '../api/domesticContainers'
import type { InventoryRecord } from '../types'
2026-08-13 11:57:44 +08:00
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
2026-10-20 10:11:31 +08:00
// Use Node's supported require export so the validator retains its constructor interop.
const ElementPlus = ( createRequire ( import . meta . url )( 'element-plus' ) as typeof import ( 'element-plus' )). default
2026-08-13 11:57:44 +08:00
const source = readFileSync ( resolve ( process . cwd (), 'src/modules/domestic-containers/pages/DomesticContainersView.vue' ), 'utf8' )
2026-08-14 10:09:51 +08:00
const summaryStyles = readFileSync ( resolve ( process . cwd (), 'src/modules/domestic-containers/pages/domesticContainersSummary.css' ), 'utf8' )
2026-08-13 11:57:44 +08:00
describe ( 'DomesticContainersView' , () => {
it ( 'uses formal shared list and centered dialog components' , () => { expect ( source ). toContain ( 'QueryToolbar' ); expect ( source ). toContain ( 'DataTable' ); expect ( source ). toContain ( 'AppDialog' ); expect ( source ). toContain ( "fixed: 'left'" ); expect ( source ). toContain ( 'width="min(720px, calc(100vw - 28px))"' ) })
it ( 'gates create actions with backend permissions' , () => { expect ( source ). toContain ( "access.can('platform:domestic-container:inbound:create')" ); expect ( source ). toContain ( "access.can('platform:domestic-container:outbound:create')" ) })
it ( 'loads only active domestic warehouse candidates and surfaces errors' , () => { expect ( source ). toContain ( "listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })" ); expect ( source ). toContain ( '国内仓候选加载失败' ) })
it ( 'keeps idempotency key stable through retry and handles conflicts by refreshing inventory' , () => { expect ( source ). toContain ( 'idempotencyKey.value' ); expect ( source ). toContain ( "submitError.value.code === 'A0409'" ); expect ( source ). toContain ( 'await loadCandidates()' ); expect ( source ). not . toContain ( 'localStorage' ) })
2026-10-20 23:34:11 +08:00
it ( 'keeps business tabs in the list toolbar with keyboard interaction and separate filters' , () => {
const toolbar = source . match ( /<template #toolbar-start>([\s\S]*?)<\/template>/ ) ? .[ 1 ]
const filters = source . match ( /<template #filters>([\s\S]*?)<\/template>/ ) ? .[ 1 ]
expect ( toolbar ). toContain ( 'class="status-tabs density-status-tabs workbench-list-tabs"' )
expect ( toolbar ). toContain ( 'role="tablist"' )
expect ( toolbar ). toContain ( 'role="tab"' )
expect ( toolbar ). toContain ( '@click="switchTab(tab.value)"' )
expect ( toolbar ). toContain ( '@keydown="handleTabKeydown($event, index)"' )
expect ( toolbar ). not . toContain ( '<h1>' )
expect ( filters ). toContain ( 'class="business-query-panel integrated-query-group"' )
expect ( filters ). toContain ( '<QueryToolbar' )
expect ( filters ). not . toContain ( 'role="tablist"' )
expect ( source ). not . toContain ( '<el-tabs' )
})
2026-08-14 10:09:51 +08:00
it ( 'keeps desktop filters and actions on one row with explicit field widths' , () => { expect ( source ). toContain ( "key: 'warehouseId', type: 'select', width: 210" ); expect ( source ). toContain ( "key: 'orderNo', type: 'input', width: 220" ); expect ( source ). toContain ( "key: 'containerNo', type: 'input', width: 220" ); expect ( source ). toContain ( "key: 'cargoName', type: 'input', width: 220" ); expect ( source ). toContain ( "key: 'status', type: 'select', width: 180" ) })
it ( 'uses the shared query toolbar flow without a page-level layout override' , () => { expect ( source ). not . toContain ( '.query-toolbar__filters' ) })
2026-09-14 15:33:35 +08:00
it ( 'keeps all four inventory facts in the approved compact summary' , () => {
for ( const fact of [ 'summary.inboundBoxes' , 'summary.outboundBoxes' , 'summary.availableBoxes' , 'summary.inboundVolumeCbm' ]) expect ( source ). toContain ( fact )
expect ( source ). toContain ( "import './domesticContainersSummary.css'" )
expect ( summaryStyles ). toContain ( 'display: flex; flex-wrap: wrap' )
2026-09-14 17:54:21 +08:00
expect ( summaryStyles ). toContain ( 'font-size: var(--admin-font-section, 14px)' )
2026-09-14 15:33:35 +08:00
expect ( summaryStyles ). not . toContain ( 'position: absolute' )
})
2026-09-04 12:02:38 +08:00
it ( 'does not offer an inventory detail action before an inbound has formed inventory' , () => { expect ( source ). toContain ( "canViewInventory(row as ContainerOrder | InventoryRecord)" ); expect ( source ). toContain ( "row.status === 'COMPLETED'" ) })
it ( 'matches backend numeric and container-number input boundaries before submit' , () => { expect ( source ). toContain ( "pattern: /^[A-Za-z0-9-]{4,32}$/" ); expect ( source ). toContain ( ':max="999999999"' ); expect ( source ). toContain ( ':max="999999999999.9999"' ) })
it ( 'requires a refreshed inventory version for completed outbound submission' , () => { expect ( source ). toContain ( "outboundForm.inventoryVersion == null" ); expect ( source ). toContain ( '库存版本未就绪,请重新选择集装箱' ) })
2026-08-13 11:57:44 +08:00
})
2026-10-20 10:11:31 +08:00
vi . mock ( 'vue-router' , () => ({ useRouter : () => ({ push : vi.fn () }) }))
vi . mock ( '@/core/access/accessPreview' , () => ({ useAccessPreviewStore : () => ({ can : () => true }) }))
vi . mock ( '@/modules/warehouses' , () => ({ listAllPlatformWarehouses : vi.fn (). mockResolvedValue ([{ id : 'warehouse-1' , warehouseCode : 'WH-01' , warehouseName : '测试仓' , warehouseType : 'DOMESTIC' , status : 'ACTIVE' }]) }))
vi . mock ( '../api/domesticContainers' , () => ({ createDomesticContainerInbound : vi.fn (), createDomesticContainerOutbound : vi.fn (), getDomesticContainerInventory : vi.fn (), listDomesticContainerInbounds : vi.fn (), listDomesticContainerInventories : vi.fn (), listDomesticContainerOutbounds : vi.fn () }))
const inventory : InventoryRecord = { id : 'inventory-1' , warehouseId : 'warehouse-1' , warehouseName : '测试仓' , containerNo : 'MSCU1234567' , cargoName : '测试货物' , availableBoxes : 8 , inboundBoxes : 10 , outboundBoxes : 2 , inboundVolumeCbm : 2.5 , inventoryStatus : 'PARTIALLY_OUTBOUND' , version : 3 , updatedAt : '2026-09-30T10:00:00' }
let mounted : VueWrapper | undefined
async function openCreate ( label : string ) {
const view = mount ( DomesticContainersView , { attachTo : document.body , global : { plugins : [ ElementPlus ] } })
mounted = view
await flushPromises ()
if ( label === '新建出库单' ) await view . findAll ( '[role="tab"]' ). find ( tab => tab . text () === '整柜出库单' ) ! . trigger ( 'click' )
await flushPromises ()
await view . findAll ( 'button' ). find ( button => button . text () === label ) ! . trigger ( 'click' )
await flushPromises ()
const form = view . findComponent ({ name : 'ElForm' })
return { view , form }
}
async function submitCreate() {
document . querySelector < HTMLButtonElement >( '.el-dialog__footer .el-button--primary' ) ! . click ()
await flushPromises ()
}
describe ( 'DomesticContainersView create workflows' , () => {
beforeEach (() => {
vi . clearAllMocks ()
const empty = { records : [], page : 1 , size : 20 , total : 0 , pages : 0 }
vi . mocked ( api . listDomesticContainerInbounds ). mockResolvedValue ( empty )
vi . mocked ( api . listDomesticContainerOutbounds ). mockResolvedValue ( empty )
vi . mocked ( api . listDomesticContainerInventories ). mockResolvedValue ({ ... empty , records : [ inventory ], summary : { inboundBoxes : 10 , outboundBoxes : 2 , availableBoxes : 8 , inboundVolumeCbm : 2.5 } })
vi . mocked ( api . getDomesticContainerInventory ). mockResolvedValue ({ inventory : { ... inventory , availableBoxes : 6 , version : 9 }, inboundOrderCount : 1 , outboundOrderCount : 1 , transactions : [] })
})
afterEach (() => { mounted ? . unmount (); mounted = undefined ; document . body . innerHTML = '' })
it ( 'keeps validation, normalized input and the idempotency key when retrying an inbound' , async () => {
vi . mocked ( api . createDomesticContainerInbound ). mockRejectedValueOnce ( new Error ( '临时失败' )). mockResolvedValueOnce ({ id : 'order-internal' , orderNo : 'ZGRK-01' , replayed : false })
const { form } = await openCreate ( '新建入库单' )
await submitCreate ()
expect ( api . createDomesticContainerInbound ). not . toHaveBeenCalled ()
const fields = form . findAllComponents ({ name : 'ElFormItem' })
const warehouse = fields . find ( item => item . props ( 'prop' ) === 'warehouseId' ) ! . findComponent ({ name : 'ElSelect' })
warehouse . vm . $emit ( 'update:modelValue' , 'warehouse-1' )
await fields . find ( item => item . props ( 'prop' ) === 'cargoName' ) ! . find ( 'input' ). setValue ( ' 测试货物 ' )
await fields . find ( item => item . props ( 'prop' ) === 'containerNo' ) ! . find ( 'input' ). setValue ( 'mscu1234567' )
fields . find ( item => item . props ( 'prop' ) === 'boxCount' ) ! . findComponent ({ name : 'ElInputNumber' }). vm . $emit ( 'update:modelValue' , 4 )
fields . find ( item => item . props ( 'prop' ) === 'volumeCbm' ) ! . findComponent ({ name : 'ElInputNumber' }). vm . $emit ( 'update:modelValue' , 1.2345 )
await flushPromises ()
await submitCreate ()
expect ( document . body . textContent ). toContain ( '整柜单据创建失败' )
await submitCreate ()
const calls = vi . mocked ( api . createDomesticContainerInbound ). mock . calls
expect ( calls ). toHaveLength ( 2 )
expect ( calls [ 0 ][ 0 ]). toEqual ( calls [ 1 ][ 0 ])
expect ( calls [ 1 ][ 0 ]). toMatchObject ({ warehouseId : 'warehouse-1' , cargoName : '测试货物' , containerNo : 'MSCU1234567' , boxCount : 4 , volumeCbm : 1.2345 , status : 'COMPLETED' , idempotencyKey : expect.any ( String ) })
})
it ( 'shows inventory facts as text and submits the refreshed version only within available stock' , async () => {
vi . mocked ( api . createDomesticContainerOutbound ). mockResolvedValue ({ id : 'order-internal' , orderNo : 'ZGCK-01' , replayed : false })
const { form } = await openCreate ( '新建出库单' )
const fields = form . findAllComponents ({ name : 'ElFormItem' })
const warehouse = fields . find ( item => item . props ( 'prop' ) === 'warehouseId' ) ! . findComponent ({ name : 'ElSelect' })
warehouse . vm . $emit ( 'update:modelValue' , 'warehouse-1' )
warehouse . vm . $emit ( 'change' , 'warehouse-1' )
await flushPromises ()
const selection = fields . find ( item => item . props ( 'prop' ) === 'containerNo' ) ! . findComponent ({ name : 'ElSelect' })
selection . vm . $emit ( 'update:modelValue' , 'inventory-1' )
selection . vm . $emit ( 'change' , 'inventory-1' )
await flushPromises ()
const cargo = fields . find ( item => item . props ( 'label' ) === '货物名称' ) !
expect ( cargo . text ()). toContain ( '测试货物' )
expect ( cargo . find ( 'input' ). exists ()). toBe ( false )
expect ( fields . find ( item => item . props ( 'label' ) === '可用库存' ) ! . text ()). toContain ( '6 箱' )
const quantity = fields . find ( item => item . props ( 'prop' ) === 'boxCount' ) ! . findComponent ({ name : 'ElInputNumber' })
quantity . vm . $emit ( 'update:modelValue' , 7 )
await flushPromises ()
await submitCreate ()
expect ( api . createDomesticContainerOutbound ). not . toHaveBeenCalled ()
expect ( document . body . textContent ). toContain ( '库存不足,当前可用 6 箱' )
quantity . vm . $emit ( 'update:modelValue' , 5 )
await flushPromises ()
await submitCreate ()
expect ( api . createDomesticContainerOutbound ). toHaveBeenCalledExactlyOnceWith ( expect . objectContaining ({ warehouseId : 'warehouse-1' , containerNo : 'MSCU1234567' , boxCount : 5 , inventoryVersion : 9 , idempotencyKey : expect.any ( String ) }))
})
})