2026-10-11 14:14:49 +08:00
import { flushPromises , mount } from '@vue/test-utils'
2026-10-11 15:33:09 +08:00
import { beforeEach , expect , it , vi } from 'vitest'
2026-10-11 14:14:49 +08:00
import { onBeforeRouteLeave , onBeforeRouteUpdate } from 'vue-router'
import InboundOrdersView from './InboundOrdersView.vue'
2026-10-15 17:11:03 +08:00
import { getFbaAppointment , updateFbaAppointment , listInboundOrders } from '../api/inboundOrders'
const routing = vi . hoisted (() => ({ push : vi.fn () }))
vi . mock ( 'vue-router' , () => ({ useRouter : () => routing , onBeforeRouteLeave : vi.fn (), onBeforeRouteUpdate : vi.fn () }))
2026-10-11 15:33:09 +08:00
const access = vi . hoisted (() => ({ canWrite :true }))
vi . mock ( '@/core/access/accessPreview' , () => ({ useAccessPreviewStore : () => ({ can : ( permission :string ) => ! permission . endsWith ( ':fba-appointment' ) || access . canWrite }) }))
2026-10-11 14:14:49 +08:00
vi . mock ( '../api/inboundOrders' , () => ({
listInboundOrders : vi.fn (). mockResolvedValue ({ records : [{ lineId : '1' , planNo : 'IP-1' , customerId : 'A' , appointmentNo : 'AP-1' , fbaWarehouseCode : 'ONT8' }], total :1 }),
getFbaAppointment : vi.fn (), updateFbaAppointment : vi.fn (), createFbaAppointment : vi.fn (), updateInboundPlan : vi.fn (),
}))
vi . mock ( 'element-plus' , () => ({ ElMessage : { warning :vi.fn (), success :vi.fn ()} }))
2026-10-11 15:33:09 +08:00
const render = () => mount ( InboundOrdersView ,{ global : { directives : { loading : {}}, stubs : {
2026-10-11 14:14:49 +08:00
AdminPage : { template : '<main><slot/></main>' }, ListPageLayout : { template : '<div><slot/></div>' },
DataTable : { props : [ 'data' ], template : '<div><slot v-for="row in data" name="actions" :row="row"/></div>' },
2026-10-11 15:33:09 +08:00
AppDialog : { name : 'AppDialog' , props : [ 'modelValue' , 'beforeClose' , 'showConfirm' ], template : '<section v-if="modelValue"><slot/><button data-close @click="$emit(\'update:model-value\',false)">关闭</button><button v-if="showConfirm !== false" data-save @click="$emit(\'confirm\')">保存</button></section>' },
2026-10-20 10:11:31 +08:00
ElButton : { template : '<button><slot/></button>' }, ElAlert : { props : [ 'title' ], template : '<p role="status">{{ title }}</p>' }, FbaAppointmentFields :true , SmartDispatchDialog :true , QueryToolbar :true , OperationErrorToast :true ,
2026-10-11 14:14:49 +08:00
}}})
2026-10-11 15:33:09 +08:00
beforeEach (() => { vi . clearAllMocks (); access . canWrite = true ; vi . mocked ( getFbaAppointment ). mockResolvedValue ({ id : '1' , appointmentNo : 'AP-1' , version :2 , originWarehouseId : '100' , fbaWarehouseCode : 'ONT8' , appointmentStatus : 'PENDING' , selectedCount :1 , items : []})})
it ( 'opens linked appointment details for list-only users and blocks saving' , async () => {
access . canWrite = false
const wrapper = render (); await flushPromises ()
expect ( wrapper . findAll ( 'button' ). some ( b => b . text () === '预约' )). toBe ( false )
await wrapper . findAll ( 'button' ). find ( b => b . text () === '查看预约' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( getFbaAppointment ). toHaveBeenCalledWith ( 'AP-1' )
expect ( wrapper . findComponent ({ name : 'FbaAppointmentFields' }). props ( 'disabled' )). toBe ( true )
expect ( wrapper . find ( '[data-save]' ). exists ()). toBe ( false )
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( updateFbaAppointment ). not . toHaveBeenCalled (); wrapper . unmount ()
})
it ( 'keeps the inbound appointment retry across close, reopen and route attempts' , async () => {
vi . mocked ( updateFbaAppointment ). mockRejectedValueOnce ( new Error ( 'connection lost' )). mockResolvedValueOnce ({} as never )
const wrapper = render ()
2026-10-11 14:14:49 +08:00
await flushPromises ()
const open = () => wrapper . findAll ( 'button' ). find ( b => b . text () === '预约' ) ! . trigger ( 'click' )
await open (); await flushPromises (); await wrapper . get ( '[data-save]' ). trigger ( 'click' ); await flushPromises ()
const original = vi . mocked ( updateFbaAppointment ). mock . calls [ 0 ] ! [ 1 ]
await wrapper . get ( '[data-close]' ). trigger ( 'click' )
const dialog = wrapper . findAllComponents ({ name : 'AppDialog' })[ 0 ] !
const done = vi . fn (); dialog . props ( 'beforeClose' )( done ); expect ( done ). not . toHaveBeenCalled ()
expect ( wrapper . find ( '[data-save]' ). exists ()). toBe ( true )
await open (); await flushPromises (); expect ( getFbaAppointment ). toHaveBeenCalledTimes ( 1 )
2026-10-21 14:30:33 +08:00
for ( const hook of [ onBeforeRouteLeave , onBeforeRouteUpdate ]) expect ( await ( vi . mocked ( hook ). mock . calls [ 0 ] ! [ 0 ] as () => Promise < boolean >)()). toBe ( false )
2026-10-11 14:14:49 +08:00
await wrapper . get ( '[data-save]' ). trigger ( 'click' ); await flushPromises ()
expect ( updateFbaAppointment ). toHaveBeenNthCalledWith ( 2 , 'AP-1' , original )
expect ( wrapper . find ( '[data-save]' ). exists ()). toBe ( false )
2026-10-21 14:30:33 +08:00
expect ( await ( vi . mocked ( onBeforeRouteLeave ). mock . calls [ 0 ] ! [ 0 ] as () => Promise < boolean >)()). toBe ( true )
2026-10-11 14:14:49 +08:00
wrapper . unmount ()
})
2026-10-15 17:11:03 +08:00
it ( 'renders MULTIPLE as multiple appointments and opens the exact source/customer instead of choosing an arbitrary appointment' , async () => {
vi . mocked ( listInboundOrders ). mockResolvedValueOnce ({ records : [{ lineId : '1' , planId : 'plan-1' , planNo : 'IP-1' , orderId : '9007199254740993' , customerId : 'customer-a' , appointmentNo :null , appointmentStatus : 'MULTIPLE' , fbaWarehouseCode : 'ONT8' }], total :1 , page :1 , size :10 , pages :1 })
const wrapper = render (); await flushPromises ()
const button = wrapper . findAll ( 'button' ). find ( button => button . text () === '管理多批预约' ) !
expect ( button . exists ()). toBe ( true )
expect ( wrapper . findAll ( 'button' ). some ( button => button . text () === '查看预约' || button . text () === '预约' )). toBe ( false )
await button . trigger ( 'click' ); await flushPromises ()
expect ( routing . push ). toHaveBeenCalledWith ({ path : '/appointment-calendar' , query : { orderId : '9007199254740993' , customerId : 'customer-a' , destinationKey : 'FBA:ONT8' }})
expect ( getFbaAppointment ). not . toHaveBeenCalled (); wrapper . unmount ()
})
it ( 'keeps replaced appointment snapshots read-only in the inbound dialog' , async () => {
vi . mocked ( getFbaAppointment ). mockResolvedValueOnce ({ id : '1' , appointmentNo : 'AP-1' , batchKey : 'FBA_APPOINTMENT:1' , replacementBatchKeys : [ 'FBA_APPOINTMENT:2' ], replacementAppointmentNos : [ 'AP-NEW' ], version :3 , originWarehouseId : '100' , fbaWarehouseCode : 'ONT8' , appointmentStatus : 'PENDING' , selectedCount :1 , items : []})
const wrapper = render (); await flushPromises ()
await wrapper . findAll ( 'button' ). find ( button => button . text () === '预约' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( wrapper . text ()). toContain ( '替代预约:AP-NEW' )
expect ( wrapper . find ( '[data-save]' ). exists ()). toBe ( false )
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( updateFbaAppointment ). not . toHaveBeenCalled (); wrapper . unmount ()
})