import { flushPromises, shallowMount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' import { ElMessage } from 'element-plus' import * as api from '../api/overseasMasterData' import ExchangeRatesView from './ExchangeRatesView.vue' import OverseasQuotesView from './OverseasQuotesView.vue' import VehiclesView from './VehiclesView.vue' vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn(), warning: vi.fn(), error: vi.fn() }, ElMessageBox: { confirm: vi.fn().mockResolvedValue(undefined) }, })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('../api/overseasMasterData', () => ({ listQuotes: vi.fn(), listQuoteWarehouseOptions: vi.fn(), getQuote: vi.fn(), createQuote: vi.fn(), updateQuote: vi.fn(), changeQuoteStatus: vi.fn(), exportQuotes: vi.fn(), listExchangeRates: vi.fn(), getExchangeRateSummary: vi.fn(), getExchangeRate: vi.fn(), createExchangeRate: vi.fn(), updateExchangeRate: vi.fn(), changeExchangeRateStatus: vi.fn(), exportExchangeRates: vi.fn(), listVehicles: vi.fn(), getVehicle: vi.fn(), createVehicle: vi.fn(), updateVehicle: vi.fn(), changeVehicleStatus: vi.fn(), exportVehicles: vi.fn(), })) const longId = '9223372036854775807' const quote = { id: longId, name: '真实报价', warehouseId: '9223372036854775806', warehouseName: '合作仓', warehouseCode: 'US-LAX', countryCode: 'US', providerName: 'Provider', currency: 'USD' as const, effectiveStart: '2026-08-01', effectiveEnd: '2026-12-31', versionCode: 'V1', status: 'INACTIVE' as const, updatedAt: '2026-08-24T10:00:00', updatedBy: 'admin', recordVersion: 7, remark: null } const quoteDetail = { summary: quote, warehouseFees: [{ item: '入库', billingUnit: 'PALLET', unitPrice: 1.25, minimumCharge: null, remark: null }], transitPrices: [], createdBy: 'admin', createdAt: '2026-08-24T09:00:00' } const rate = { month: '2026-08', baseCurrency: 'USD', quoteCurrency: 'CNY', usdToCnyAverage: 7.12345678, status: 'ACTIVE' as const, recordVersion: 3, updatedBy: 'admin', updatedAt: '2026-08-24T10:00:00', remark: null } const vehicle = { id: longId, plateNumber: 'CA-01', model: 'Truck', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90, fuelConsumptionPer100Km: 12.5, mileageKm: 101, driverOrDispatchCompany: null, phone: null, region: 'OVERSEAS' as const, ownership: 'OWNED' as const, status: 'IN_SERVICE' as const, remark: null, recordVersion: 4, updatedBy: 'admin', updatedAt: '2026-08-24T10:00:00' } const emptyPage = { records: [], page: 1, size: 20, total: 0, pages: 0 } const slot = { template: '
' } const stubs = { AdminPage: slot, ListPageLayout: slot, QueryToolbar: true, LoadErrorState: true, DataTable: { props: ['data'], template: '
', }, AppDialog: { props: ['modelValue'], emits: ['confirm', 'update:modelValue'], template: '
', }, ElButton: { emits: ['click'], template: '' }, ElForm: slot, ElFormItem: slot, ElInput: true, ElSelect: slot, ElOption: true, ElDatePicker: true, ElInputNumber: true, ElTable: slot, ElTableColumn: true, ElTag: slot, } const mountView = (component: object) => shallowMount(component, { global: { stubs } }) const button = (wrapper: ReturnType, label: string) => { const found = wrapper.findAll('button').find((item) => item.text().includes(label)) expect(found, `button ${label}`).toBeDefined() return found! } describe('overseas master data failed writes preserve editor context', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(api.listQuotes).mockResolvedValue({ ...emptyPage, records: [quote], total: 1, pages: 1 }) vi.mocked(api.listQuoteWarehouseOptions).mockResolvedValue([{ warehouseId: quote.warehouseId, warehouseCode: 'US-LAX', warehouseName: '合作仓', countryCode: 'US' }]) vi.mocked(api.getQuote).mockResolvedValue(quoteDetail) vi.mocked(api.listExchangeRates).mockResolvedValue({ ...emptyPage, records: [rate], total: 1, pages: 1 }) vi.mocked(api.getExchangeRateSummary).mockResolvedValue({ latestMonth: rate.month, latestUsdToCnyAverage: rate.usdToCnyAverage, updatedBy: 'admin', updatedAt: rate.updatedAt }) vi.mocked(api.getExchangeRate).mockResolvedValue(rate) vi.mocked(api.listVehicles).mockResolvedValue({ ...emptyPage, records: [vehicle], total: 1, pages: 1 }) vi.mocked(api.getVehicle).mockResolvedValue(vehicle) }) it.each([ [OverseasQuotesView, ['详情', '编辑', '停用']], [VehiclesView, ['查看', '编辑', '状态']], [ExchangeRatesView, ['编辑', '停用']], ])('keeps all available actions visible without an unnecessary overflow menu', async (component, labels) => { const wrapper = mountView(component) await flushPromises() const actions = wrapper.get('.density-actions') const expected = component === OverseasQuotesView ? ['详情', '编辑', '启用'] : labels expect(actions.findAll('button').map(button => button.text().trim())).toEqual(expected) expect(actions.findAll('button').every(button => button.classes().includes('density-action'))).toBe(true) expect(actions.find('el-dropdown-stub').exists()).toBe(false) }) it('keeps quote edit open, sends only update fields, and shows A0409 request ID', async () => { vi.mocked(api.updateQuote).mockRejectedValue(new ApiResponseError('记录版本已变化', 'A0409', 200, null, 'req-quote-409')) const wrapper = mountView(OverseasQuotesView) await flushPromises() await button(wrapper, '编辑').trigger('click') await flushPromises() await wrapper.get('[data-test="dialog-confirm"]').trigger('click') await flushPromises() expect(api.updateQuote).toHaveBeenCalledWith(longId, expect.objectContaining({ recordVersion: 7, name: quote.name })) const payload = vi.mocked(api.updateQuote).mock.calls[0]?.[1] expect(payload).not.toHaveProperty('warehouseId') expect(payload).not.toHaveProperty('versionCode') expect(wrapper.find('[data-test="dialog"]').exists()).toBe(true) expect(ElMessage.error).toHaveBeenCalledWith(expect.stringContaining('req-quote-409')) }) it('keeps exchange-rate decimal/version in the open editor after A0409', async () => { vi.mocked(api.updateExchangeRate).mockRejectedValue(new ApiResponseError('汇率版本已变化', 'A0409', 200, null, 'req-rate-409')) const wrapper = mountView(ExchangeRatesView) await flushPromises() await button(wrapper, '编辑').trigger('click') await flushPromises() await wrapper.get('[data-test="dialog-confirm"]').trigger('click') await flushPromises() expect(api.updateExchangeRate).toHaveBeenCalledWith('2026-08', { recordVersion: 3, usdToCnyAverage: 7.12345678, remark: null }) expect(wrapper.find('[data-test="dialog"]').exists()).toBe(true) expect(ElMessage.error).toHaveBeenCalledWith(expect.stringContaining('req-rate-409')) }) it('keeps vehicle long ID, decimal and recordVersion in the open editor after A0409', async () => { vi.mocked(api.updateVehicle).mockRejectedValue(new ApiResponseError('车辆版本已变化', 'A0409', 200, null, 'req-vehicle-409')) const wrapper = mountView(VehiclesView) await flushPromises() await button(wrapper, '编辑').trigger('click') await flushPromises() await wrapper.get('[data-test="dialog-confirm"]').trigger('click') await flushPromises() expect(api.updateVehicle).toHaveBeenCalledWith(longId, expect.objectContaining({ recordVersion: 4, fuelConsumptionPer100Km: 12.5, mileageKm: 101 })) expect(wrapper.find('[data-test="dialog"]').exists()).toBe(true) expect(ElMessage.error).toHaveBeenCalledWith(expect.stringContaining('req-vehicle-409')) }) })