Files
admin-web/src/modules/overseas-master-data/pages/CapacityOffersBehavior.test.ts
T

282 lines
26 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 CapacityInquiryTable from '../components/CapacityInquiryTable.vue'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { ElMessageBox } from 'element-plus'
import CapacityOffersView from './CapacityOffersView.vue'
import CapacityOfferDetailView from './CapacityOfferDetailView.vue'
import CapacityInquiryDialog from '../components/CapacityInquiryDialog.vue'
import CapacityOfferSettingsDialog from '../components/CapacityOfferSettingsDialog.vue'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
import * as api from '../api/capacityOffers'
import type { CapacityOffer, CapacityInquiry } from '../capacityOfferTypes'
import type { FleetTrip } from '../tripTypes'
const state = vi.hoisted(() => ({ manage: false, write: true, quote: false, links: new Set<string>(), route: { name: 'overseas-master-data-capacity-offer-detail', params: { offerNo: 'CO-1' } }, push: vi.fn() }))
vi.mock('vue-router', () => ({ useRoute: () => state.route, useRouter: () => ({ push: state.push }) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (p: string) => state.links.has(p) || (p === 'platform:fleet:capacity-offer:manage' ? state.manage : p === 'platform:fleet:quote:list' ? state.quote : p === 'platform:fleet:capacity-offer:list' || p.includes(':inquiry-') && state.write) }) }))
vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn(), warning: vi.fn() }, ElMessageBox: { confirm: vi.fn().mockResolvedValue(undefined) } }))
vi.mock('../api/capacityOffers', () => ({ listCapacityOffers: vi.fn(), listCapacityInquiries: vi.fn(), getCapacityOffer: vi.fn(), getCapacityInquiry: vi.fn(), saveCapacityInquiry: vi.fn(), saveAndConfirmInquiry: vi.fn(), commandCapacityInquiry: vi.fn(), listOfferCustomers: vi.fn(), listOfferQuotes: vi.fn(), saveCapacityOffer: vi.fn(), commandCapacityOffer: vi.fn() }))
export const offer: CapacityOffer = { offerNo: 'CO-1', tripNo: 'TR-1', status: 'OPEN', recordVersion: 2, tripRecordVersion: 5, warehouseId: '92', warehouseName: '测试起运仓', plannedDepartureAt: '2026-10-10T12:00:00Z', cutoffAt: '2026-10-10T10:00:00Z', contactName: '调度', contactPhone: '测试联系信息', remark: null, capacityKnown: false, canCreateInquiry: true, canConfirm: false, blockReason: '待调度核对', destinations: [{ destinationKey: 'FBA:ONT8', name: 'ONT8', deliveryIndex: 1, remaining: null }], segments: [], actions: { canEdit: true, canPublish: false, canPause: true, canResume: false, canClose: true } }
export const inquiry: CapacityInquiry = { bookingNo: 'FB-1', offerNo: 'CO-1', tripNo: 'TR-1', status: 'DRAFT', recordVersion: 1, offerRecordVersion: 2, tripRecordVersion: 5, salesOwnerUserId: '17', customerId: '19', customerCode: 'C-19', customerName: '测试客户', businessType: 'US_TRUCK', deliveryMode: 'SELF_DELIVERY', estimatedArrivalAt: null, expectedPalletCount: null, cargo: { destinationKey: 'FBA:ONT8', cargoName: null, packageCount: null, palletSpaces: null, weightKg: null, volumeCbm: null, priceBasis: 'PALLET', unitPrice: null, currency: 'USD', contactName: null, contactPhone: null, remark: null }, destinationLabel: 'ONT8', agreedAmount: null, quoteSource: null, businessOrderNo: null, incomingNo: null, dispatchNo: null, updatedAt: '2026-10-07T00:00:00Z', actions: { canEdit: true, canConfirm: false, canCancel: true, blockReason: null }, appointmentCoverage: null, progress: { stage: 'NEEDS_DETAILS', label: '待补资料', reservation: '尚未保位', owner: '销售', primaryAction: 'EDIT', message: '确认前还需补充资料', missingFields: ['预计重量'] } }
const pageOf = <T,>(records: T[], total = records.length) => ({ records, total, page: 1, size: 20, pages: Math.ceil(total / 20) })
const wrappers: VueWrapper[] = []
const layout = { template: '<div><slot name="header"/><slot/></div>' }
const field = { props: ['modelValue'], emits: ['update:modelValue', 'focus'], template: '<input />' }
function mount(component: object, props: Record<string, unknown> = {}): { wrapper: VueWrapper; pinia: ReturnType<typeof createPinia> } {
const pinia = createPinia()
const wrapper = shallowMount(component, { props, global: { plugins: [pinia], directives: { loading: () => undefined }, stubs: {
AdminPage: layout, ListPageLayout: layout, DetailPageLayout: layout,
DataTable: { name: 'DataTable', props: ['data', 'page', 'pageSize', 'total'], emits: ['update:page'], template: '<div><slot name="toolbar-start"/><slot name="filters"/><slot name="notices"/><div v-for="(row, index) in data" :key="index"><slot name="loads" :row="row"/></div><slot/></div>' },
QueryToolbar: { name: 'QueryToolbar', props: ['modelValue', 'fields'], emits: ['update:modelValue', 'search'], template: '<div/>' },
CapacityInquiryTable: { name: 'CapacityInquiryTable', props: ['rows', 'total', 'failed'], emits: ['action', 'update:page'], template: '<div><slot name="toolbar-start"/><slot name="filters"/><slot name="notices"/></div>' },
CapacityInquiryDialog: component === CapacityInquiryDialog ? false : true,
AppDialog: { name: 'AppDialog', props: ['modelValue', 'showConfirm', 'confirmText', 'beforeClose'], emits: ['update:modelValue', 'confirm'], template: '<div v-if="modelValue"><slot/></div>' },
ElButton: { props: ['disabled', 'loading'], template: '<button :disabled="disabled || loading"><slot/></button>' }, ElForm: layout, ElFormItem: layout,
ElInput: { ...field, name: 'ElInput' }, ElSelect: { ...field, name: 'ElSelect' }, ElInputNumber: { ...field, name: 'ElInputNumber' }, ElDatePicker: { ...field, name: 'ElDatePicker' }, ElOption: true, ApiErrorMeta: true, LoadErrorState: true,
} } }); wrappers.push(wrapper); return { wrapper, pinia }
}
const button = (w: VueWrapper, label: string) => w.findAll('button').find(b => b.text() === label)!
beforeEach(() => { vi.clearAllMocks(); state.manage = false; state.write = true; state.quote = false; state.links.clear(); vi.mocked(ElMessageBox.confirm).mockImplementation(vi.fn().mockResolvedValue(undefined)); vi.mocked(api.listCapacityOffers).mockResolvedValue(pageOf([structuredClone(offer)], 41)); vi.mocked(api.listCapacityInquiries).mockResolvedValue(pageOf([structuredClone(inquiry)])); vi.mocked(api.getCapacityOffer).mockResolvedValue({ offer: structuredClone(offer), inquiries: pageOf([structuredClone(inquiry)]) }) })
afterEach(() => { wrappers.splice(0).forEach(w => w.unmount()); vi.useRealTimers() })
it.each(['DRAFT', 'CONFIRMED', 'RECEIVED', 'BOUND', 'CANCELLED'] as const)('labels %s cargo consistently in the list and detail without treating estimated pallets as actual', async status => {
const row: CapacityInquiry = { ...inquiry, status, expectedPalletCount: 7, cargo: { ...inquiry.cargo, packageCount: 12, palletSpaces: 3, weightKg: 80, volumeCbm: 2 } }
const label = status === 'RECEIVED' || status === 'BOUND' ? '已核实' : status === 'CANCELLED' ? '记录' : '预报'
const list = shallowMount(CapacityInquiryTable, { props: { rows: [row], total: 1, loading: false }, global: { stubs: {
DataTable: { props: ['data', 'columns'], template: '<div><slot name="cargo" :row="data[0]"/></div>' },
} } })
wrappers.push(list)
expect(list.text()).toContain(`${label}:12 箱 · 3 托位 · 80 kg · 2 m³`)
expect(list.text()).toContain('预计实托数:7')
expect(list.text()).not.toContain('7 实托')
await list.setProps({ rows: [{ ...row, expectedPalletCount: null, cargo: { ...row.cargo, packageCount: null } }] })
expect(list.text()).toContain(`${label}:— 箱`)
expect(list.text()).toContain('预计实托数:—')
const { wrapper: detail } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: row, readonly: true })
await flushPromises()
for (const suffix of ['箱数', '标准托位', '重量(kg)', '体积(m³)']) expect(detail.find(`[label="${label}${suffix}"]`).exists()).toBe(true)
expect(detail.findAll('summary').map(item => item.text())).toContain(label === '已核实' ? '已核实货量与预计实托数' : `${label}货量`)
expect(detail.find('[label="预计实托数"]').exists()).toBe(true)
expect(detail.find('[label="已核实实托数"]').exists()).toBe(false)
expect(api.saveCapacityInquiry).not.toHaveBeenCalled()
})
it('loads only the active tab and sends exactly one query after resetting from a later page', async () => {
const { wrapper } = mount(CapacityOffersView); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenCalledOnce(); expect(api.listCapacityInquiries).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('待调度核对')
wrapper.findComponent({ name: 'DataTable' }).vm.$emit('update:page', 2); await flushPromises()
wrapper.findComponent({ name: 'QueryToolbar' }).vm.$emit('search'); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenCalledTimes(3); expect(api.listCapacityOffers).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1 }))
await button(wrapper, '我的登记').trigger('click'); await flushPromises()
expect(api.listCapacityInquiries).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'MINE' })); expect(api.getCapacityOffer).not.toHaveBeenCalled()
const query = wrapper.findComponent({ name: 'QueryToolbar' }); query.vm.$emit('update:modelValue', { scope: 'ALL' }); await flushPromises(); query.vm.$emit('search'); await flushPromises()
expect(api.listCapacityInquiries).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'MINE' }))
})
it('ignores an older page response after changing tabs and retains an explicit error state', async () => {
let resolve!: (value: ReturnType<typeof pageOf<CapacityOffer>>) => void
vi.mocked(api.listCapacityOffers).mockReturnValue(new Promise(done => { resolve = done }))
const { wrapper } = mount(CapacityOffersView); await button(wrapper, '我的登记').trigger('click'); await flushPromises()
resolve(pageOf([offer], 900)); await flushPromises()
expect(wrapper.findComponent({ name: 'CapacityInquiryTable' }).props('total')).toBe(1)
vi.mocked(api.listCapacityInquiries).mockRejectedValue(new Error('读取失败')); wrapper.findComponent({ name: 'QueryToolbar' }).vm.$emit('search'); await flushPromises()
expect(wrapper.findComponent({ name: 'LoadErrorState' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'CapacityInquiryTable' }).props('failed')).toBe(true)
})
it('reads detail once, permits drafting while capacity is unknown, and never preloads customers or quotes', async () => {
const { wrapper } = mount(CapacityOfferDetailView); await flushPromises()
expect(api.getCapacityOffer).toHaveBeenCalledOnce(); expect(api.listCapacityInquiries).not.toHaveBeenCalled()
await button(wrapper, '登记客户货物').trigger('click'); await flushPromises()
expect(wrapper.findComponent({ name: 'CapacityInquiryDialog' }).props('modelValue')).toBe(true)
expect(api.listOfferCustomers).not.toHaveBeenCalled(); expect(api.listOfferQuotes).not.toHaveBeenCalled()
})
it('preserves nullable estimates and retries the original immutable create request after an uncertain result', async () => {
vi.mocked(api.saveCapacityInquiry).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce({ offer, inquiry, replayed: true })
const { wrapper, pinia } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: null, readonly: false }); await flushPromises()
const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0]!.vm.$emit('update:modelValue', inquiry.customerId); selects[1]!.vm.$emit('update:modelValue', inquiry.cargo.destinationKey); await flushPromises()
const dialog = wrapper.findComponent({ name: 'AppDialog' }); dialog.vm.$emit('confirm'); await flushPromises()
const first = vi.mocked(api.saveCapacityInquiry).mock.calls[0]![2]
expect(first).toMatchObject({ expectedPalletCount: null, cargo: { packageCount: null, palletSpaces: null }, idempotencyKey: expect.any(String) })
expect(useUnsavedWorkStore(pinia).findBlocking(new Set(), true)).toHaveLength(1)
dialog.vm.$emit('confirm'); await flushPromises()
expect(api.saveCapacityInquiry).toHaveBeenNthCalledWith(2, offer.offerNo, null, first)
expect(wrapper.emitted('saved')).toHaveLength(1); expect(api.listOfferCustomers).not.toHaveBeenCalled()
})
it('protects X/Esc/cancel model-close requests and does not emit close when discard is declined', async () => {
vi.mocked(ElMessageBox.confirm).mockRejectedValueOnce('cancel')
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry, readonly: false }); await flushPromises()
wrapper.findAllComponents({ name: 'ElInput' })[0]!.vm.$emit('update:modelValue', '改名'); await flushPromises()
const done = vi.fn()
await wrapper.findComponent({ name: 'AppDialog' }).props('beforeClose')(done); await flushPromises()
expect(ElMessageBox.confirm).toHaveBeenCalledOnce(); expect(done).not.toHaveBeenCalled(); expect(wrapper.emitted('update:modelValue')).toBeUndefined()
})
it('retains an uncertain request and route protection after closing and reopening its form', async () => {
vi.mocked(api.saveCapacityInquiry).mockRejectedValue(new Error('network'))
const { wrapper, pinia } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry, readonly: false }); await flushPromises()
wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
const first = vi.mocked(api.saveCapacityInquiry).mock.calls[0]![2]
await (wrapper as VueWrapper<Record<string, unknown>>).setProps({ modelValue: false }); await flushPromises()
expect(useUnsavedWorkStore(pinia).findBlocking(new Set(), true)).toHaveLength(1)
await (wrapper as VueWrapper<Record<string, unknown>>).setProps({ modelValue: true }); await flushPromises()
expect(useUnsavedWorkStore(pinia).findBlocking(new Set(), true)).toHaveLength(1)
wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
expect(api.saveCapacityInquiry).toHaveBeenNthCalledWith(2, offer.offerNo, inquiry.bookingNo, first)
})
it('keeps sales from confirming even if a stale row advertises the action', async () => {
const row = { ...inquiry, actions: { ...inquiry.actions, canConfirm: true } }
const { wrapper } = mount(CapacityOffersView); await flushPromises(); await button(wrapper, '我的登记').trigger('click'); await flushPromises()
wrapper.findComponent({ name: 'CapacityInquiryTable' }).vm.$emit('action', 'confirm', row); await flushPromises()
expect(api.commandCapacityInquiry).not.toHaveBeenCalled(); expect(ElMessageBox.confirm).not.toHaveBeenCalled()
})
it('loads publishing settings only when explicitly opened, not with the trip page', async () => {
state.manage = true
const { wrapper } = mount(CapacityOfferSettingsDialog, { modelValue: false, trip: { tripNo: 'TR-1', recordVersion: 5 } as FleetTrip }); await flushPromises()
expect(api.listCapacityOffers).not.toHaveBeenCalled(); await (wrapper as VueWrapper<Record<string, unknown>>).setProps({ modelValue: true }); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenCalledExactlyOnceWith({ scope: 'ALL', tripNo: 'TR-1', page: 1, size: 1 })
})
it('retries an uncertain combined save and confirmation with the original payload', async () => {
state.manage = true
const row = { ...inquiry, actions: { ...inquiry.actions, canConfirm: true } }
vi.mocked(api.saveAndConfirmInquiry).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce({ offer, inquiry: { ...row, status: 'CONFIRMED' }, replayed: true })
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: row, readonly: false, confirmMode: true }); await flushPromises()
const dialog = wrapper.findComponent({ name: 'AppDialog' })
dialog.vm.$emit('confirm'); await flushPromises()
const first = vi.mocked(api.saveAndConfirmInquiry).mock.calls[0]!
expect(first[0]).toBe(row.bookingNo)
expect(wrapper.emitted('saved')).toBeUndefined()
dialog.vm.$emit('confirm'); await flushPromises()
expect(api.saveAndConfirmInquiry).toHaveBeenNthCalledWith(2, ...first)
expect(api.saveCapacityInquiry).not.toHaveBeenCalled()
expect(wrapper.emitted('saved')).toHaveLength(1)
})
it('keeps the create button disabled with a reason after cutoff, while hiding it without write permission', async () => {
vi.mocked(api.getCapacityOffer).mockResolvedValue({ offer: { ...offer, canCreateInquiry: false, blockReason: '交货截止已过或不符合当前发车时间' }, inquiries: pageOf([]) })
const { wrapper } = mount(CapacityOfferDetailView); await flushPromises()
expect(button(wrapper, '登记客户货物').attributes('disabled')).toBeDefined()
expect(wrapper.text()).toContain('无法登记客户货物:交货截止已过')
expect(wrapper.findComponent({ name: 'CapacityInquiryDialog' }).props('modelValue')).toBe(false)
state.write = false
const readonly = mount(CapacityOfferDetailView); await flushPromises()
expect(readonly.wrapper.findAll('button').some(node => node.text() === '登记客户货物')).toBe(false)
})
it('defaults minimal managers to their TODO inquiries without loading offers, and explicitly switches to available offers', async () => {
state.manage = true
const { wrapper } = mount(CapacityOffersView); await flushPromises()
expect(api.listCapacityInquiries).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'TODO' }))
expect(api.listCapacityOffers).not.toHaveBeenCalled()
expect(state.links.size).toBe(0)
await button(wrapper, '可拼车次').trigger('click'); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'AVAILABLE' }))
const query = wrapper.findComponent({ name: 'QueryToolbar' })
query.vm.$emit('update:modelValue', { scope: 'ALL' }); await flushPromises(); query.vm.$emit('search'); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'ALL' }))
query.vm.$emit('update:modelValue', {}); await flushPromises(); query.vm.$emit('search'); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'AVAILABLE' }))
})
it('automatically selects a single destination and preserves unknown quantities when only the customer is known', async () => {
vi.mocked(api.saveCapacityInquiry).mockResolvedValue({ offer, inquiry, replayed: false })
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: null, readonly: false }); await flushPromises()
wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', '19'); await flushPromises()
wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
expect(api.saveCapacityInquiry).toHaveBeenCalledWith(offer.offerNo, null, expect.objectContaining({ customerId: '19', cargo: expect.objectContaining({ destinationKey: 'FBA:ONT8', packageCount: null, palletSpaces: null, weightKg: null, volumeCbm: null }) }))
})
it('requires an explicit choice for multiple destinations', async () => {
const many = { ...offer, destinations: [...offer.destinations, { ...offer.destinations[0]!, destinationKey: 'FBA:ACY2', name: 'ACY2', deliveryIndex: 2 }] }
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer: many, inquiry: null, readonly: false }); await flushPromises()
wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', '19'); await flushPromises()
wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
expect(api.saveCapacityInquiry).not.toHaveBeenCalled()
})
// Navigation must retain the exact bound business references and independent read permissions.
it.each([CapacityOffersView, CapacityOfferDetailView])('opens the bound trip separately from read-only dispatch detail', async component => {
const row = { ...inquiry, dispatchNo: 'DS-BOUND', status: 'BOUND' as const }
state.links = new Set(['platform:fleet:trip:list', 'platform:overseas-warehouse:dispatch:list'])
const { wrapper } = mount(component); await flushPromises()
const dialog = wrapper.findComponent(CapacityInquiryDialog)
dialog.vm.$emit('action', 'dispatch', row); await flushPromises()
expect(state.push).toHaveBeenLastCalledWith({ name: 'overseas-dispatch-detail', params: { dispatchNo: 'DS-BOUND' } })
dialog.vm.$emit('action', 'trip', row); await flushPromises()
expect(state.push).toHaveBeenLastCalledWith({ name: 'overseas-master-data-trip-detail', params: { tripNo: row.tripNo } })
expect(api.commandCapacityInquiry).not.toHaveBeenCalled()
state.links.clear(); state.push.mockClear()
dialog.vm.$emit('action', 'trip', row); dialog.vm.$emit('action', 'dispatch', row); await flushPromises()
expect(state.push).not.toHaveBeenCalled()
})
it('shows follow-up destinations only with the matching read permission', async () => {
const row = { ...inquiry, dispatchNo: 'DS-BOUND', status: 'BOUND' as const }
state.links.add('platform:overseas-warehouse:dispatch:list')
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: row, readonly: true }); await flushPromises()
expect(button(wrapper, '查看排车详情')).toBeDefined()
expect(button(wrapper, '前往车次审核')).toBeUndefined()
wrapper.unmount()
state.links = new Set(['platform:fleet:trip:list'])
const second = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: row, readonly: true }); await flushPromises()
expect(button(second.wrapper, '查看排车详情')).toBeUndefined()
await button(second.wrapper, '前往车次审核').trigger('click')
expect(second.wrapper.emitted('action')).toEqual([['trip', row]])
expect(api.commandCapacityInquiry).not.toHaveBeenCalled()
})
it('keeps initial inputs compact, reveals confirmation fields, and keeps unknown amounts null', async () => {
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: null, readonly: false })
await flushPromises()
const visibleInputs = wrapper.findAll('input').filter(input => !input.element.closest('details:not([open])'))
expect(visibleInputs).toHaveLength(2)
expect(wrapper.text()).toContain('业务类型:本土散板')
const cargo = wrapper.findAll('details').find(section => section.find('summary').text().startsWith('已知货量'))!
expect(cargo.attributes('open')).toBeUndefined()
await wrapper.setProps({ confirmMode: true }); await flushPromises()
expect(cargo.attributes('open')).toBeDefined()
expect(wrapper.findAll('details').find(section => section.find('summary').text().startsWith('确认承运前待补资料'))!.attributes('open')).toBeDefined()
})
it('highlights only the allowed current step and keeps other actions secondary', async () => {
state.manage = true
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry, readonly: true })
await flushPromises()
const section = wrapper.find('[aria-label="继续处理本票货物"]')
expect(section.findAll('button').filter(item => !item.element.closest('details')).map(item => item.text())).toEqual(['补充资料'])
expect(section.find('details').text()).toContain('核对并确认')
await button(wrapper, '补充资料').trigger('click')
expect(wrapper.emitted('action')?.[0]).toEqual(['edit', inquiry])
await wrapper.setProps({ inquiry: { ...inquiry, progress: { ...inquiry.progress, primaryAction: 'INCOMING' }, actions: { ...inquiry.actions, canEdit: false, canConfirm: false } } })
expect(section.findAll('button').filter(item => !item.element.closest('details'))).toHaveLength(0)
state.links.add('platform:fleet:trip:booking-edit')
await wrapper.setProps({ inquiry: { ...inquiry, progress: { ...inquiry.progress, primaryAction: 'INCOMING' } } })
expect(section.findAll('button').filter(item => !item.element.closest('details')).map(item => item.text())).toEqual(['安排来货'])
})
it('shows the selected destination capacity when confirming and never treats unknown capacity as zero', async () => {
state.manage = true
const knownOffer = { ...offer, capacityKnown: true, destinations: [
{ ...offer.destinations[0]!, remaining: { palletSpaces: 0, weightKg: 12, volumeCbm: 3 } },
{ destinationKey: 'FBA:ACY2', name: 'ACY2', deliveryIndex: 2, remaining: { palletSpaces: 7, weightKg: 800, volumeCbm: 20 } },
] }
const { wrapper } = mount(CapacityInquiryDialog, { modelValue: true, offer: knownOffer, inquiry, readonly: false, confirmMode: true })
await flushPromises()
expect(wrapper.get('[aria-label="本目的站可接余位"]').text()).toContain('0 托位 · 12 kg · 3 m³')
wrapper.findAllComponents({ name: 'ElSelect' })[1]!.vm.$emit('update:modelValue', 'FBA:ACY2'); await flushPromises()
expect(wrapper.get('[aria-label="本目的站可接余位"]').text()).toContain('7 托位 · 800 kg · 20 m³')
await wrapper.setProps({ offer: { ...knownOffer, capacityKnown: false } })
expect(wrapper.get('[aria-label="本目的站可接余位"]').text()).toContain('待调度核对')
expect(wrapper.get('[aria-label="本目的站可接余位"]').text()).not.toContain('800')
expect(api.saveAndConfirmInquiry).not.toHaveBeenCalled()
})
it('prioritizes the allowed current list action and keeps other actions in the overflow menu', async () => {
state.manage = true
const wrapper = shallowMount(CapacityInquiryTable, { props: { rows: [inquiry], total: 1, loading: false }, global: { directives: { loading: () => undefined }, stubs: {
DataTable: { props: ['data'], template: '<div><slot name="actions" :row="data[0]"/></div>' },
RowActions: { name: 'RowActions', props: ['actions', 'maxVisible'], template: '<div/>' },
} } })
wrappers.push(wrapper)
const actions = () => wrapper.findComponent({ name: 'RowActions' }).props('actions') as { key: string; visible?: boolean; priority?: number }[]
const primary = () => actions().filter(item => item.visible !== false).sort((a,b) => (b.priority || 0) - (a.priority || 0))[0]?.key
expect(wrapper.findComponent({ name: 'RowActions' }).props('maxVisible')).toBe(1)
expect(primary()).toBe('edit')
await wrapper.setProps({ rows: [{ ...inquiry, progress: { ...inquiry.progress, primaryAction: 'INCOMING' }, actions: { ...inquiry.actions, canEdit: false, canConfirm: false } }] })
expect(primary()).toBe('view')
state.links.add('platform:fleet:trip:booking-edit')
await wrapper.setProps({ rows: [{ ...inquiry, progress: { ...inquiry.progress, primaryAction: 'INCOMING' } }] })
expect(primary()).toBe('incoming')
})