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

126 lines
8.0 KiB
TypeScript
Raw Normal View History

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: '<div><slot/><slot name="header"/><slot name="toolbar"/><slot name="metrics"/><slot name="state"/></div>' }
const stubs = {
AdminPage: slot,
ListPageLayout: slot,
QueryToolbar: true,
LoadErrorState: true,
DataTable: {
props: ['data'],
template: '<div><slot name="toolbar-actions"/><slot v-if="data && data[0]" name="actions" :row="data[0]"/></div>',
},
AppDialog: {
props: ['modelValue'], emits: ['confirm', 'update:modelValue'],
template: '<div v-if="modelValue" data-test="dialog"><slot/><button data-test="dialog-confirm" @click="$emit(\'confirm\')">confirm</button></div>',
},
ElButton: { emits: ['click'], template: '<button @click="$emit(\'click\')"><slot/></button>' },
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<typeof mountView>, 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)
})
2026-09-15 09:58:15 +08:00
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'))
})
})