282 lines
26 KiB
TypeScript
282 lines
26 KiB
TypeScript
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')
|
||
})
|