feat(fleet): select versioned sales quotes and isolate cost drafts

This commit is contained in:
2026-10-03 13:20:35 +08:00
parent 3440f9dd11
commit 5641ea0f7e
15 changed files with 288 additions and 31 deletions
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils'
import TripCostPanel from './TripCostPanel.vue'
import * as api from '../api/tripCosts'
import { ApiResponseError } from '@/shared/api/http'
const access = vi.hoisted(() => ({ read: true, write: true }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (permission: string) => permission.endsWith(':list') ? access.read : access.write }) }))
vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn(), warning: vi.fn() } }))
@@ -21,7 +22,7 @@ function mount() {
} } }); wrappers.push(wrapper); return wrapper
}
function button(w: VueWrapper, label: string) { return w.findAll('button').find(b => b.text() === label)! }
beforeEach(() => { vi.clearAllMocks(); access.read = true; access.write = true; vi.mocked(api.listTripCosts).mockResolvedValue(structuredClone(result)); vi.mocked(api.createTripCost).mockResolvedValue(cost); vi.mocked(api.updateTripCost).mockResolvedValue(cost)
beforeEach(() => { vi.resetAllMocks(); access.read = true; access.write = true; vi.mocked(api.listTripCosts).mockResolvedValue(structuredClone(result)); vi.mocked(api.createTripCost).mockResolvedValue(cost); vi.mocked(api.updateTripCost).mockResolvedValue(cost)
vi.mocked(api.listTripCostHistory).mockResolvedValue({ records: [{ recordVersion: '9', action: 'UPDATE', reason: '更正', occurredAt: cost.updatedAt, snapshot: { cost: { ...cost, recordVersion: '9' }, summary: result.summary } }], page: 1, size: 1, total: 1, pages: 1 }) })
afterEach(() => wrappers.splice(0).forEach(w => w.unmount()))
it('loads one scoped page and summary without per-row reads or invented zero totals', async () => {
@@ -67,7 +68,7 @@ it('keeps the successful save authoritative when pagination changes while it is
expect(api.listTripCosts).toHaveBeenLastCalledWith('TR-ONE', 1, 10)
expect(api.createTripCost).toHaveBeenCalledTimes(1)
})
it('does not let an old save clear a new pending save after switching away and back', async () => {
it('retains the original request across trip switches and ignores the first response during its retry', async () => {
let finishFirst!: (value: api.TripCost) => void
let finishSecond!: (value: api.TripCost) => void
vi.mocked(api.createTripCost)
@@ -77,6 +78,8 @@ it('does not let an old save clear a new pending save after switching away and b
w.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 12.5)
await button(w, '保存').trigger('click'); await flushPromises()
await w.setProps({ tripNo: 'TR-TWO' }); await flushPromises()
await button(w, '登记成本').trigger('click'); await flushPromises()
expect(w.findAllComponents({ name: 'AppDialog' })[0]!.props('modelValue')).toBe(false)
await w.setProps({ tripNo: 'TR-ONE' }); await flushPromises()
await button(w, '登记成本').trigger('click'); await flushPromises()
w.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 20)
@@ -84,9 +87,22 @@ it('does not let an old save clear a new pending save after switching away and b
finishFirst(cost); await flushPromises()
await button(w, '保存').trigger('click'); await flushPromises()
expect(api.createTripCost).toHaveBeenCalledTimes(2)
expect(vi.mocked(api.createTripCost).mock.calls[1]).toEqual(vi.mocked(api.createTripCost).mock.calls[0])
finishSecond(cost); await flushPromises()
expect(w.findAllComponents({ name: 'AppDialog' })[0]!.props('modelValue')).toBe(false)
})
it('unlocks a definite HTTP200 business rejection for correction with a fresh create key', async () => {
vi.mocked(api.createTripCost).mockRejectedValueOnce(new ApiResponseError('业务拒绝', 'A0422', 200))
const w = mount(); await flushPromises(); await button(w, '登记成本').trigger('click'); await flushPromises()
w.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 12.5)
await button(w, '保存').trigger('click'); await flushPromises()
const first = vi.mocked(api.createTripCost).mock.calls[0]!
expect(w.text()).not.toContain('正在核对上次登记结果')
w.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 10)
await button(w, '保存').trigger('click'); await flushPromises()
const second = vi.mocked(api.createTripCost).mock.calls[1]!
expect(second[1]).not.toBe(first[1]); expect(second[2].amount).toBe(10)
})
it('reads the current version before editing and avoids writing unchanged contents', async () => {
const w = mount(); await flushPromises(); await button(w, '更正').trigger('click'); await flushPromises()
expect(api.listTripCostHistory).toHaveBeenCalledExactlyOnceWith('TR-ONE', 'TC-ONE', 1, 1)