Files
admin-web/src/modules/overseas-warehouse/pages/DispatchesView.test.ts
T

383 lines
26 KiB
TypeScript

import { createPinia } from 'pinia'
import ElementPlus, { ElMessage, ElFormItem, ElDatePicker, ElSelect, ElCheckbox } from 'element-plus'
import AppDialog from '@/shared/components/AppDialog.vue'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import QueryToolbar, { type QueryModel } from '@/shared/components/QueryToolbar.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import RowActions from '@/shared/components/RowActions.vue'
import { cancelDispatch, getDispatchBolPdf, getDispatchExternalFreight, getDispatchFeeCoverage, listDispatches, removeDispatchItem, transferDispatchToDelivery, updateDispatch } from '../api/dispatches'
import DeliveryFeeCoveragePanel from '../components/DeliveryFeeCoveragePanel.vue'
import DispatchesView from './DispatchesView.vue'
vi.mock('@/modules/warehouses', () => ({
listAllPlatformWarehouses: vi.fn(async (query: { keyword: string }) => [{ id: query.keyword, warehouseCode: query.keyword, warehouseType: 'FBA' }]),
getPlatformWarehouse: vi.fn(async (id: string) => ({ warehouse: { warehouseCode: id, warehouseType: id === '321' ? 'OVERSEAS' : 'FBA' }, addresses: [{ addressType: 'OPERATING', addressLine1: '100 Example Street', city: 'City', stateProvince: 'CA', countryCode: 'US' }], capabilities: [] })),
}))
const push = vi.hoisted(() => vi.fn())
vi.mock('vue-router', () => ({ useRouter: () => ({ push }), useRoute: () => ({ query: {} }) }))
const canPermission = vi.hoisted(() => vi.fn<(permissionCode: string) => boolean>(() => true))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) }))
vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn(), removeDispatchItem: vi.fn(), getDispatchBolPdf: vi.fn(), getDispatchExternalFreight: vi.fn(), getDispatchFeeCoverage: vi.fn(), updateDispatch: vi.fn(), cancelDispatch: vi.fn(), transferDispatchToDelivery: vi.fn() }))
class ResizeObserverStub implements ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
function memoryStorage(): Storage {
const values = new Map<string, string>()
return {
get length() { return values.size },
clear: () => values.clear(),
getItem: (key) => values.get(key) ?? null,
key: (index) => [...values.keys()][index] ?? null,
removeItem: (key) => values.delete(key),
setItem: (key, value) => values.set(key, value),
}
}
enableAutoUnmount(afterEach)
afterEach(() => vi.restoreAllMocks())
afterAll(() => vi.unstubAllGlobals())
const allowedActions = { canEdit: true, editBlockReason: null, canCancel: true, cancelBlockReason: null,
canRemovePallet: true, removePalletBlockReason: null, canTransfer: true, transferBlockReason: null, outboundNo: null, outboundStatus: null }
describe('DispatchesView', () => {
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('localStorage', memoryStorage())
vi.mocked(listDispatches).mockReset().mockResolvedValue({
records: [{
id: '190000000000000099', dispatchNo: 'PC-1', appointmentBatches: [], actions: allowedActions, vehiclePlate: 'CA-100', driverName: '张师傅',
driverPhone: '555-0100', carrierName: 'Carrier A', orderNos: 'LM-1,LM-2', route: 'ONT8,LAX9',
overseasWarehouseId: '321', overseasWarehouseName: '洛杉矶仓', departAt: '2026-08-20T09:00:00', fbaAppointmentNo: 'FBA-1',
fbaAppointmentStatus: 'CONFIRMED', vehicleAppointmentStatus: 'BOOKED', estimatedArrivalAt: '2026-08-20T18:00:00',
status: 'PENDING_DEPARTURE', palletCount: 12, packageCount: 80, createdAt: '2026-08-19T16:00:00',
}],
page: 1, size: 20, total: 1, pages: 1,
})
vi.mocked(updateDispatch).mockReset()
vi.mocked(getDispatchBolPdf).mockReset().mockResolvedValue(new Blob(['%PDF-test'], { type: 'application/pdf' }))
vi.mocked(removeDispatchItem).mockReset()
vi.mocked(cancelDispatch).mockReset()
vi.mocked(transferDispatchToDelivery).mockReset().mockResolvedValue({ dispatchNo: 'PC-1', outboundNo: 'OUT-1', outboundStatus: 'WAIT_OUTBOUND', palletCount: 12, packageCount: 80, deliveries: [], replayed: false })
vi.mocked(getDispatchExternalFreight).mockReset().mockResolvedValue({ snapshotHash: 'own-hash', requiresDeliveryFee: true, quotes: [] })
vi.mocked(getDispatchFeeCoverage).mockReset().mockResolvedValue({ dispatchNo: 'PC-1', sourceComplete: true, blockReason: null, cargoHash: 'cargo-hash', cargo: [], feeSources: [] })
canPermission.mockReset().mockReturnValue(true)
})
it('queries stages on the server and discards an older response after switching', async () => {
let resolveOld!: (value: Awaited<ReturnType<typeof listDispatches>>) => void
vi.mocked(listDispatches).mockImplementationOnce(() => new Promise(resolve => { resolveOld = resolve }))
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await wrapper.get('[role="tablist"]').findAll('button')[2]!.trigger('click')
await flushPromises()
expect(listDispatches).toHaveBeenLastCalledWith(expect.objectContaining({ stage: 'LOADING', page: 1, status: undefined }))
expect(wrapper.text()).toContain('PC-1')
resolveOld({ records: [], total: 0, page: 1, size: 10, pages: 0 })
await flushPromises()
expect(wrapper.text()).toContain('PC-1')
await wrapper.get('[role="tablist"]').findAll('button')[3]!.trigger('click')
await flushPromises()
expect(listDispatches).toHaveBeenLastCalledWith(expect.objectContaining({ stage: 'AFTER_LOADING', page: 1 }))
})
it('shows the shared route map only in the after-loading vehicle section without another query', async () => {
const wrapper = mount(DispatchesView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
expect(wrapper.text()).not.toContain('查看路线')
await wrapper.get('[aria-label="排车执行阶段"]').findAll('button')[3]!.trigger('click')
await flushPromises()
const requests = vi.mocked(listDispatches).mock.calls.length
await wrapper.findAll('button').find(button => button.text() === '查看路线')!.trigger('click')
await flushPromises()
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('modelValue')).toBe(true)
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('origin')).toEqual({ kind: 'warehouse', label: '洛杉矶仓', warehouseId: '321' })
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('destinations')).toEqual(['ONT8', 'LAX9'].map(code => ({ kind: 'warehouse', label: code, warehouseCode: code })))
expect(listDispatches).toHaveBeenCalledTimes(requests)
await wrapper.get('[aria-label="排车执行阶段"]').findAll('button')[1]!.trigger('click')
await flushPromises()
await wrapper.get('[aria-label="排车业务分区"]').findAll('button')[1]!.trigger('click')
expect(wrapper.findAll('button').some(button => button.text() === '查看路线')).toBe(false)
})
it('shows review facts and opens the matching trip section with read permission', async () => {
const result = await listDispatches({ page: 1, size: 10 })
result.records[0]!.workflow = { tripNo: 'TR-A', dispatchReview: { status: 'REJECTED', reason: '请核对货量', reviewedAt: null }, loadingReview: { status: 'PENDING', reason: null, reviewedAt: null } }
vi.mocked(listDispatches).mockClear().mockResolvedValue(result)
canPermission.mockImplementation(code => code === 'platform:fleet:trip:list')
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
const sections = wrapper.get('[aria-label="排车业务分区"]')
expect(sections.findAll('button').map(button => button.text())).toEqual(['车辆信息'])
await wrapper.get('[aria-label="排车执行阶段"]').findAll('button')[1]!.trigger('click')
await flushPromises()
expect(sections.findAll('button').map(button => button.text())).toEqual(['车辆信息', '调度审核', '理货员审核'])
expect(wrapper.findAll('[role="tablist"]')).toHaveLength(2)
expect(wrapper.text()).not.toContain('请核对货量')
await sections.findAll('button')[1]!.trigger('click')
await flushPromises()
expect(wrapper.text()).toContain('请核对货量')
expect(wrapper.findAll('button').some(button => button.text() === '打印BOL')).toBe(false)
await wrapper.findAll('button').find(button => button.text() === '查看审核')!.trigger('click')
expect(push).toHaveBeenLastCalledWith({ name: 'overseas-master-data-trip-detail', params: { tripNo: 'TR-A' }, query: { tab: 'overview' } })
await sections.findAll('button')[2]!.trigger('click')
await flushPromises()
expect(wrapper.text()).not.toContain('请核对货量')
expect(wrapper.text()).toContain('待审核')
const links = wrapper.findAll('button').filter(button => button.text() === '查看审核')
await links[0]!.trigger('click')
expect(push).toHaveBeenLastCalledWith({ name: 'overseas-master-data-trip-detail', params: { tripNo: 'TR-A' }, query: { tab: 'loading' } })
expect(wrapper.text()).not.toContain('前往审核')
expect(listDispatches).toHaveBeenCalledTimes(2)
})
it.each([0, 2, 3, 4].flatMap(stageIndex => [1, 2].map(sectionIndex => [stageIndex, sectionIndex])))('leaving a review section for stage %s resets section %s to vehicle information', async (stageIndex, sectionIndex) => {
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
const stages = wrapper.get('[aria-label="排车执行阶段"]').findAll('button')
const sections = wrapper.get('[aria-label="排车业务分区"]')
await stages[1]!.trigger('click')
await flushPromises()
await sections.findAll('button')[sectionIndex]!.trigger('click')
expect(wrapper.findAll('button').some(button => button.text() === '打印BOL')).toBe(false)
await stages[stageIndex]!.trigger('click')
await flushPromises()
expect(sections.findAll('button').map(button => button.text())).toEqual(['车辆信息'])
expect(sections.get('button').attributes('aria-selected')).toBe('true')
expect(wrapper.findAll('button').some(button => button.text() === '打印BOL')).toBe(true)
expect(wrapper.findAll('button').some(button => ['前往审核', '查看审核'].includes(button.text()))).toBe(false)
await stages[1]!.trigger('click')
await flushPromises()
expect(sections.findAll('button').map(button => button.text())).toEqual(['车辆信息', '调度审核', '理货员审核'])
expect(sections.get('button').attributes('aria-selected')).toBe('true')
})
it.each([
[[], ['查看', '打印BOL'], false],
[['edit'], ['查看', '打印BOL', '编辑'], false],
[['cancel'], ['查看', '打印BOL', '取消'], false],
[['transfer-delivery'], ['查看', '打印BOL', '转派送'], false],
[['edit', 'transfer-delivery'], ['查看', '打印BOL', '转派送', '更多'], true],
[['edit', 'cancel'], ['查看', '打印BOL', '编辑', '更多'], true],
[['edit', 'cancel', 'transfer-delivery'], ['查看', '打印BOL', '转派送', '更多'], true],
])('uses the row action threshold for permissions %j', async (allowed, labels, overflow) => {
canPermission.mockImplementation(code => allowed.some(action => code === 'platform:overseas-warehouse:dispatch:' + action))
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
const actions = wrapper.get('.el-table__body-wrapper .density-actions')
expect(actions.findAll('button').map(button => button.text().trim())).toEqual(labels)
expect(actions.find('.el-dropdown').exists()).toBe(overflow)
})
it('keeps the integrated header and filters usable through failure and retry', async () => {
vi.mocked(listDispatches).mockRejectedValueOnce(new Error('网络错误'))
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
const card = wrapper.get('.data-table--integrated')
expect(card.get('.data-table__toolbar h1').text()).toBe('排车列表')
expect(card.get('.data-table__toolbar button[aria-label="列设置"]').text()).toBe('字段设置')
expect(card.get('.data-table__filters').findComponent(QueryToolbar).exists()).toBe(true)
expect(card.find('.el-table').exists()).toBe(false)
expect(card.find('.data-table__pagination').exists()).toBe(false)
wrapper.findComponent(LoadErrorState).vm.$emit('retry')
await flushPromises()
expect(card.find('.data-table__state').exists()).toBe(false)
expect(card.get('.el-table').text()).toContain('PC-1')
expect(card.get('.data-table__pagination').text()).toContain('共 1 条排车记录')
})
it('loads immediately and sends only supported page filters', async () => {
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
expect(listDispatches).toHaveBeenCalledWith({ page: 1, size: 10, dispatchNo: undefined, vehiclePlate: undefined, status: undefined, fbaAppointmentNo: undefined })
const model: QueryModel = {
dispatchNo: 'PC-1', vehiclePlate: 'CA-100',
status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1',
}
wrapper.findComponent(QueryToolbar).vm.$emit('search', model)
await flushPromises()
expect(listDispatches).toHaveBeenCalledWith({
page: 1, size: 10, dispatchNo: 'PC-1',
vehiclePlate: 'CA-100', status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1',
})
expect(wrapper.text()).toContain('PC-1')
expect(wrapper.text()).toContain('待发车')
expect(wrapper.text()).toContain('FBA-1 · 已确认')
expect(wrapper.text()).toContain('已预约')
expect(wrapper.get('.data-table').classes()).not.toContain('admin-text-actions')
expect(wrapper.get('.query-toolbar').classes()).not.toContain('admin-text-actions')
expect(wrapper.get('.data-table__toolbar button[aria-label="列设置"]').find('svg').exists()).toBe(true)
expect(wrapper.get('.query-toolbar__actions .el-button--primary').text()).toBe('查询')
expect(wrapper.get('button[aria-label="上一页"]').find('svg').exists()).toBe(true)
expect(wrapper.findAll('.query-toolbar__actions button svg')).toHaveLength(3)
expect(wrapper.get('.el-table__body-wrapper .row-actions').classes()).toContain('admin-text-actions')
})
it('keeps long dispatch identifiers as strings', async () => {
vi.mocked(listDispatches).mockResolvedValueOnce({
records: [{ id: '190000000000000099', dispatchNo: '190000000000000099', appointmentBatches: [] }],
page: 1, size: 20, total: 1, pages: 1,
})
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
expect(wrapper.html()).toContain('190000000000000099')
})
it('opens the server-generated BOL PDF with one request', async () => {
const pdfWindow = { location: { href: '' }, close: vi.fn() }
vi.spyOn(window, 'open').mockReturnValue(pdfWindow as unknown as Window)
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:dispatch-bol'), revokeObjectURL: vi.fn() })
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
expect(getDispatchBolPdf).not.toHaveBeenCalled()
await wrapper.findAll('.el-table__body-wrapper button').find((button) => button.text().trim() === '打印BOL')!.trigger('click')
await flushPromises()
expect(getDispatchBolPdf).toHaveBeenCalledOnce()
expect(getDispatchBolPdf).toHaveBeenCalledWith('PC-1')
expect(pdfWindow.location.href).toBe('blob:dispatch-bol')
})
it('documents the backend feature flag required by a missing endpoint', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/DispatchesView.vue'), 'utf8')
expect(source).toContain('请确认海外尾程排车接口已发布')
})
it('uses server blocks even for a pending row and offers the original trip and outbound paths', async () => {
const result = await listDispatches({ page: 1, size: 10 })
Object.assign(result.records[0]!, {
actions: { ...allowedActions, canEdit: false, canCancel: false, canRemovePallet: false, canTransfer: false,
editBlockReason: '请先在车次 TR-A 中解除关联', cancelBlockReason: '请先在车次 TR-A 中解除关联',
removePalletBlockReason: '请先在车次 TR-A 中解除关联', transferBlockReason: '原出库已取消,请取消原排车后通过新预排重新安排',
outboundNo: 'OUT-A', outboundStatus: 'CANCELLED' },
workflow: { tripNo: 'TR-A', dispatchReview: { status: 'PENDING', reason: null, reviewedAt: null }, loadingReview: { status: 'PENDING', reason: null, reviewedAt: null } },
})
vi.mocked(listDispatches).mockClear().mockResolvedValue(result)
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } }); await flushPromises()
const actions = wrapper.findAllComponents(RowActions).find(item => item.element.closest('.el-table__body-wrapper'))!
const visible = actions.props('actions').filter(action => action.visible !== false).map(action => action.key)
expect(visible.filter(key => ['edit', 'cancel', 'transfer'].includes(key))).toEqual([])
expect(visible).toEqual(expect.arrayContaining(['trip', 'outbound', 'detail']))
expect(wrapper.text()).toContain('原出库已取消')
actions.vm.$emit('action', 'edit'); actions.vm.$emit('action', 'transfer'); await flushPromises()
expect(wrapper.findAllComponents(AppDialog).filter(dialog => ['编辑排车', '转派送'].includes(String(dialog.props('title')))).every(dialog => !dialog.props('modelValue'))).toBe(true)
actions.vm.$emit('action', 'trip'); await flushPromises()
expect(push).toHaveBeenLastCalledWith({ name: 'overseas-master-data-trip-detail', params: { tripNo: 'TR-A' }, query: { tab: 'overview' } })
actions.vm.$emit('action', 'outbound'); await flushPromises()
expect(push).toHaveBeenLastCalledWith({ name: 'overseas-outbounds', query: { documentNo: 'OUT-A' } })
expect(getDispatchExternalFreight).not.toHaveBeenCalled()
})
it('keeps write commands closed when action capabilities are missing', async () => {
const result = await listDispatches({ page: 1, size: 10 })
delete result.records[0]!.actions
vi.mocked(listDispatches).mockClear().mockResolvedValue(result)
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } }); await flushPromises()
expect(wrapper.findComponent(RowActions).props('actions').filter(action => action.visible !== false).map(action => action.key)).toEqual(['detail', 'bol'])
expect(wrapper.text()).toContain('操作资格暂未返回,请刷新列表')
})
it.each([
{ replayed: true, outboundStatus: 'WAIT_OUTBOUND', method: 'info' as const, message: '本次未重复生成' },
{ replayed: true, outboundStatus: 'CANCELLED', method: 'warning' as const, message: '请取消原排车后重新预排' },
])('reports transfer replay accurately for $outboundStatus', async result => {
const success = vi.spyOn(ElMessage, 'success'), notice = vi.spyOn(ElMessage, result.method)
vi.mocked(transferDispatchToDelivery).mockResolvedValue({ dispatchNo: 'PC-1', outboundNo: 'OUT-OLD', palletCount: 1, packageCount: 2, deliveries: [], replayed: result.replayed, outboundStatus: result.outboundStatus })
vi.mocked(getDispatchExternalFreight).mockResolvedValue({ snapshotHash: 'external-hash', requiresDeliveryFee: false, quotes: [] })
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } }); await flushPromises()
await wrapper.findAll('button').find(button => button.text() === '转派送')!.trigger('click'); await flushPromises()
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '转派送')!
dialog.findComponent(ElDatePicker).vm.$emit('update:modelValue', '2026-10-03T12:00:00')
for (const [label, value] of [['派送方式', 'TRUCK_PALLET'], ['提货方式', 'WAREHOUSE_PICKUP'], ['装车类型', 'PALLET']]) {
dialog.findAllComponents(ElFormItem).find(item => item.props('label') === label)!.findComponent(ElSelect).vm.$emit('update:modelValue', value)
}
await flushPromises(); dialog.vm.$emit('confirm'); await flushPromises()
expect(notice).toHaveBeenCalledWith(expect.stringContaining(result.message))
expect(success).not.toHaveBeenCalledWith('已生成出库单和派送单')
})
it('loads external quotes only when opened and submits their hash without a duplicate delivery charge', async () => {
vi.mocked(getDispatchExternalFreight).mockResolvedValue({ snapshotHash: 'external-hash', requiresDeliveryFee: false, quotes: [
{ bookingNo: 'FB-1', recordVersion: '4', incomingNo: 'WT-1', inboundCode: 'IN-1', customerName: '客户甲', destinationKey: 'FBA:ONT8', currency: 'CNY', amount: 60, quantity: 2, quantityUnit: '托位', unitPrice: 30 },
] })
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
expect(getDispatchExternalFreight).not.toHaveBeenCalled()
await wrapper.findAll('button').find(button => button.text() === '转派送')!.trigger('click')
await flushPromises()
expect(getDispatchExternalFreight).toHaveBeenCalledOnce()
expect(getDispatchExternalFreight).toHaveBeenCalledWith('PC-1')
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '转派送')!
expect(dialog.text()).toContain('FB-1')
expect(dialog.text()).not.toContain('请输入派送费用')
dialog.findComponent(ElDatePicker).vm.$emit('update:modelValue', '2026-10-03T12:00:00')
for (const [label, value] of [['派送方式', 'TRUCK_PALLET'], ['提货方式', 'WAREHOUSE_PICKUP'], ['装车类型', 'PALLET']]) {
dialog.findAllComponents(ElFormItem).find(item => item.props('label') === label)!.findComponent(ElSelect).vm.$emit('update:modelValue', value)
}
await flushPromises()
dialog.vm.$emit('confirm')
await flushPromises()
expect(transferDispatchToDelivery).toHaveBeenCalledWith('PC-1', expect.objectContaining({ fees: [], externalFreightHash: 'external-hash' }))
})
it('confirms an original delivery fee without submitting a new receivable', async () => {
vi.mocked(getDispatchFeeCoverage).mockResolvedValue({ dispatchNo: 'PC-1', sourceComplete: true, blockReason: null, cargoHash: 'actual-cargo',
cargo: [{ dispatchItemId: '201', warehousePalletId: '501', palletNo: 'PLT-1', orderId: '401', sourceRegion: 'FIRSTLEG', sourceInboundId: '11', inboundCode: 'IN-11', packages: 20, dispatchInboundCode: 'IN-11' }],
feeSources: [{ financeFeeId: '711', sourceFeeId: '811', sourceType: 'INBOUND_FEE', sourceVersion: 3, customerId: '101', sourceInboundId: '11', inboundCode: 'IN-11', feeItemCode: 'FREIGHT', feeItemName: '原运费', currency: 'CNY', amount: 100, sourceHash: 'original-fee-hash', status: 'AVAILABLE' }],
})
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
expect(getDispatchFeeCoverage).not.toHaveBeenCalled()
await wrapper.findAll('button').find(button => button.text() === '转派送')!.trigger('click')
await flushPromises()
expect(getDispatchFeeCoverage).toHaveBeenCalledTimes(1)
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '转派送')!
await dialog.findComponent(DeliveryFeeCoveragePanel).findAllComponents(ElCheckbox).find(row => row.text() === '已含派送')!.find('input').setValue(true)
dialog.findComponent(ElDatePicker).vm.$emit('update:modelValue', '2026-10-03T12:00:00')
for (const [label, value] of [['派送方式', 'TRUCK_PALLET'], ['提货方式', 'WAREHOUSE_PICKUP'], ['装车类型', 'PALLET']]) {
dialog.findAllComponents(ElFormItem).find(item => item.props('label') === label)!.findComponent(ElSelect).vm.$emit('update:modelValue', value)
}
await flushPromises()
expect(dialog.text()).toContain('本次全部货物已覆盖:派送')
dialog.vm.$emit('confirm')
await flushPromises()
expect(transferDispatchToDelivery).toHaveBeenCalledWith('PC-1', expect.objectContaining({ fees: [], feeCoverageCargoHash: 'actual-cargo', feeCoverage: [{ financeFeeId: '711', sourceVersion: 3, sourceHash: 'original-fee-hash', services: ['DELIVERY_FEE'] }] }))
})
it('gates edit, cancel, pallet removal and transfer with independent permissions and server capabilities', async () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/DispatchesView.vue'), 'utf8')
expect(source).toContain('permission.dispatchEdit')
expect(source).toContain('permission.dispatchCancel')
expect(source).toContain('permission.dispatchTransferDelivery')
expect(source).toContain("import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'")
expect(source).toContain("{ key: 'transfer', label: '转派送', priority: 250")
expect(source).toContain('access.can(permission.dispatchEdit) && row.actions?.canEdit === true')
expect(source).toContain('access.can(permission.dispatchCancel) && row.actions?.canCancel === true')
expect(source).toContain('access.can(permission.dispatchTransferDelivery) && row.actions?.canTransfer === true')
expect(source).toContain('access.can(permission.dispatchEdit) && row.actions?.canRemovePallet === true')
expect(source).toContain('!canRemovePallet(editRow.value)')
expect(source).toContain('removeDispatchItem')
expect(source).not.toContain('托盘箱唛确认')
expect(source).not.toContain('palletCartonMarks')
expect(source).toContain('v-model="editForm.route" readonly')
expect(source).not.toContain('route: editForm.route.trim()')
expect(source).toContain('dispatchVehicleSizeOptions')
expect(source).not.toContain('departDispatch')
canPermission.mockReturnValue(false)
const wrapper = mount(DispatchesView, { global: { plugins: [createPinia(), ElementPlus] } })
await flushPromises()
expect(wrapper.text()).not.toContain('转派送')
})
})