import ElementPlus, { ElMessage } from 'element-plus' import { createPinia } from 'pinia' import { mount, flushPromises, type VueWrapper } from '@vue/test-utils' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import CapacityInquiryDialog from './CapacityInquiryDialog.vue' import { listOfferCustomers, listOfferQuotes, saveCapacityInquiry } from '../api/capacityOffers' import type { CapacityOffer, CapacityInquiry } from '../capacityOfferTypes' import type { FleetQuoteSourceSnapshot } from '../api/fleetQuoteCandidates' const permissions = vi.hoisted(() => ({ values: new Set() })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (permission: string) => permissions.values.has(permission) }) })) vi.mock('../api/capacityOffers', () => ({ listOfferCustomers: vi.fn(), listOfferQuotes: vi.fn(), saveCapacityInquiry: vi.fn() })) const offer: CapacityOffer = { offerNo: 'CO-QUOTE-POSITIVE', tripNo: 'TR-QUOTE-POSITIVE', status: 'OPEN', recordVersion: 4, tripRecordVersion: 7, warehouseId: '92', warehouseName: '测试起运仓', plannedDepartureAt: '2026-10-20T12:00:00Z', cutoffAt: '2026-10-20T10:00:00Z', contactName: null, contactPhone: null, remark: null, destinations: [{ destinationKey: 'FBA:ONT8', name: 'ONT8', deliveryIndex: 1, remaining: { palletSpaces: 8, weightKg: 1000, volumeCbm: 20 } }], segments: [], capacityKnown: true, canCreateInquiry: true, canConfirm: true, blockReason: null, actions: { canEdit: false, canPublish: false, canPause: false, canResume: false, canClose: false } } const wrappers: VueWrapper[] = [] beforeEach(() => { vi.clearAllMocks() vi.mocked(listOfferCustomers).mockResolvedValue([{ id: '19', code: 'C-19', name: '报价测试客户' }]) }) afterEach(async () => { wrappers.splice(0).forEach(wrapper => wrapper.unmount()); ElMessage.closeAll(); await flushPromises(); document.body.innerHTML = '' }) function formItem(wrapper: VueWrapper, label: string) { return wrapper.findAll('.el-form-item').find(item => item.find('.el-form-item__label').text() === label)! } async function select(wrapper: VueWrapper, label: string, value: string) { formItem(wrapper, label).findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', value) await flushPromises() } const scenarios = [ { actor: 'sales', priceBasis: 'PALLET' as const, permissions: ['platform:fleet:capacity-offer:list', 'platform:fleet:capacity-offer:inquiry-create', 'platform:fleet:quote:list'] }, { actor: 'manager', priceBasis: 'ORDER' as const, permissions: ['platform:fleet:capacity-offer:manage', 'platform:fleet:quote:list'] }, ] it.each(scenarios)('uses explicit offer quote queries, pagination and negotiated-price saving for $actor', async scenario => { permissions.values = new Set(scenario.permissions) const source: FleetQuoteSourceSnapshot = { quoteNo: 'FQ-PAGE-2', recordVersion: '9007199254740993', priceItem: scenario.priceBasis === 'ORDER' ? 'FULL_DELIVERY' : 'LOOSE_DELIVERY', customerId: '19', customerName: '报价测试客户', originCity: 'LA', destinationCode: 'ONT8', distanceMiles: 35, currency: 'CNY', priceBasis: scenario.priceBasis, quotedUnitPrice: 25.5, validFrom: '2026-10-01', validTo: '2026-10-31', effectiveOn: '2026-10-20' } vi.mocked(listOfferQuotes).mockResolvedValueOnce({ records: Array.from({ length: 20 }, (_, index) => ({ ...source, quoteNo: `FQ-PAGE-1-${index + 1}` })), total: 21, page: 1, size: 20, pages: 2 }).mockResolvedValueOnce({ records: [source], total: 21, page: 2, size: 20, pages: 2 }) vi.mocked(saveCapacityInquiry).mockImplementation(async (_offerNo, _bookingNo, body) => { const inquiry: CapacityInquiry = { bookingNo: 'FB-QUOTE-SAVED', offerNo: offer.offerNo, tripNo: offer.tripNo, status: 'DRAFT', recordVersion: 1, offerRecordVersion: offer.recordVersion, tripRecordVersion: offer.tripRecordVersion, salesOwnerUserId: '17', customerId: body.customerId, customerCode: 'C-19', customerName: '报价测试客户', businessType: body.businessType, deliveryMode: body.deliveryMode, estimatedArrivalAt: body.estimatedArrivalAt, expectedPalletCount: body.expectedPalletCount, cargo: { ...body.cargo }, destinationLabel: 'ONT8', agreedAmount: null, quoteSource: source, businessOrderNo: null, incomingNo: null, dispatchNo: null, updatedAt: '2026-10-07T00:00:00Z', actions: { canEdit: true, canConfirm: false, canCancel: true, blockReason: null } } return { offer, inquiry, replayed: false } }) const wrapper = mount(CapacityInquiryDialog, { attachTo: document.body, props: { modelValue: true, offer, inquiry: null, readonly: false }, global: { plugins: [createPinia(), ElementPlus] } }); wrappers.push(wrapper) await flushPromises() expect(listOfferQuotes).not.toHaveBeenCalled() await select(wrapper, '委托客户', '19'); await select(wrapper, '本车卸货站', 'FBA:ONT8'); await select(wrapper, '币种', 'CNY'); await select(wrapper, '计价方式', scenario.priceBasis) expect(listOfferQuotes).not.toHaveBeenCalled() const quotePanel = wrapper.get('section[aria-label="报价来源"]') await quotePanel.findAll('button').find(button => button.text() === '查询报价库')!.trigger('click'); await flushPromises() const criteria = { customerId: '19', destinationKey: 'FBA:ONT8', currency: 'CNY', priceBasis: scenario.priceBasis, page: 1, size: 20 } expect(listOfferQuotes).toHaveBeenCalledExactlyOnceWith(offer.offerNo, criteria) await quotePanel.findAll('button').find(button => button.text() === '下一页')!.trigger('click'); await flushPromises() expect(listOfferQuotes).toHaveBeenNthCalledWith(2, offer.offerNo, { ...criteria, page: 2 }) const reference = { quoteNo: source.quoteNo, recordVersion: source.recordVersion, priceItem: source.priceItem } quotePanel.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', `${source.quoteNo}:${source.recordVersion}:${source.priceItem}`); await flushPromises() const price = formItem(wrapper, scenario.priceBasis === 'PALLET' ? '每托位单价' : '整单价格').findComponent({ name: 'ElInputNumber' }) expect(price.props('modelValue')).toBe(25.5) expect(quotePanel.findComponent({ name: 'ElSelect' }).props('modelValue')).toBe(`${source.quoteNo}:${source.recordVersion}:${source.priceItem}`) expect(quotePanel.text()).toContain('来源报价 FQ-PAGE-2 · 25.50 CNY') await price.get('input').setValue('21.75'); await flushPromises() expect(price.props('modelValue')).toBe(21.75) expect(quotePanel.text()).toContain('来源报价 FQ-PAGE-2 · 25.50 CNY') await wrapper.findAll('.app-dialog__footer button').find(button => button.text() === '保存意向')!.trigger('click'); await flushPromises() expect(saveCapacityInquiry).toHaveBeenCalledExactlyOnceWith(offer.offerNo, null, expect.objectContaining({ customerId: '19', offerRecordVersion: 4, tripRecordVersion: 7, recordVersion: null, quoteRef: reference, idempotencyKey: expect.any(String), cargo: expect.objectContaining({ destinationKey: 'FBA:ONT8', currency: 'CNY', priceBasis: scenario.priceBasis, unitPrice: 21.75 }) })) expect(listOfferQuotes).toHaveBeenCalledTimes(2) expect(wrapper.emitted('saved')).toHaveLength(1) })