import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils' import TripOperationsRecordDialog from './TripOperationsRecordDialog.vue' import * as api from '../api/tripOperations' vi.mock('../api/tripOperations', () => ({ getTripOperationsRecord: vi.fn(), saveTripOperationsRecord: vi.fn(), listTripOperationsRecordHistory: vi.fn() })) const row: api.TripOperationsRow = { tripNo: 'TR1', status: 'IN_TRANSIT', vehiclePlate: 'TEST-1', driverName: '司机', warehouseName: '起运仓', departedAt: '2026-10-01T00:00:00Z', completedAt: null, destinations: [], physicalPallets: 1, palletSpaces: 1, ownPackages: 1, externalPackages: 0, externalAgreedAmounts: [], externalPalletRates: [], measured: null, financeStatus: 'NOT_QUERIED', costStatus: 'FORBIDDEN', costs: null } const record: api.TripOperationsRecord = { tripNo: 'TR1', recordVersion: '0', details: { actualDistanceMiles: null, actualFuelLitres: null, remark: null }, updatedAt: null } const wrappers: VueWrapper[] = [] const slot = { template: '
' } const input = { name: 'ElInputNumber', props: ['modelValue'], emits: ['update:modelValue'], template: '' } async function mount(readonly = false) { const wrapper = shallowMount(TripOperationsRecordDialog, { props: { modelValue: false, row, readonly }, global: { stubs: { AppDialog: { props: ['modelValue'], template: '
' }, ElForm: slot, ElFormItem: slot, ElTabs: { name: 'ElTabs', props: ['modelValue'], emits: ['update:modelValue'], template: '
' }, ElTabPane: slot, ElInputNumber: input, ElInput: { ...input, name: 'ElInput' }, ElButton: { props: ['disabled', 'loading'], template: '' }, ElAlert: { props: ['title'], template: '
{{ title }}
' }, ApiErrorMeta: true, DataTable: true, } } }); wrappers.push(wrapper); await wrapper.setProps({ modelValue: true }); await flushPromises(); return wrapper } beforeEach(() => { vi.clearAllMocks(); vi.mocked(api.getTripOperationsRecord).mockResolvedValue(record); vi.mocked(api.listTripOperationsRecordHistory).mockResolvedValue({ records: [], total: 0, page: 1, size: 20, pages: 0 }) }) afterEach(() => wrappers.splice(0).forEach(w => w.unmount())) it('loads current record on open, history only when selected, and hides writes for readers', async () => { const wrapper = await mount(true) expect(api.getTripOperationsRecord).toHaveBeenCalledExactlyOnceWith('TR1'); expect(api.listTripOperationsRecordHistory).not.toHaveBeenCalled() expect(wrapper.text()).not.toContain('保存记录') wrapper.findComponent({ name: 'ElTabs' }).vm.$emit('update:modelValue', 'history'); await flushPromises() expect(api.listTripOperationsRecordHistory).toHaveBeenCalledExactlyOnceWith('TR1', 1, 20) }) it('unknown write retry preserves original key, version and measured values', async () => { const wrapper = await mount(); vi.mocked(api.saveTripOperationsRecord).mockRejectedValueOnce(new Error('network')) wrapper.findAllComponents({ name: 'ElInputNumber' })[0]!.vm.$emit('update:modelValue', 12.5); await flushPromises() await wrapper.findAll('button').find(b => b.text() === '保存记录')!.trigger('click'); await flushPromises() const original = vi.mocked(api.saveTripOperationsRecord).mock.calls[0]![1] expect(original).toMatchObject({ recordVersion: '0', details: { actualDistanceMiles: 12.5, actualFuelLitres: null } }) wrapper.findAllComponents({ name: 'ElInputNumber' })[0]!.vm.$emit('update:modelValue', 99) vi.mocked(api.saveTripOperationsRecord).mockResolvedValue({ ...record, recordVersion: '1' }) await wrapper.findAll('button').find(b => b.text() === '按原请求重试')!.trigger('click'); await flushPromises() expect(vi.mocked(api.saveTripOperationsRecord).mock.calls[1]![1]).toEqual(original); expect(wrapper.emitted('saved')).toHaveLength(1) }) it('a failed initial read cannot save a guessed record version', async () => { vi.mocked(api.getTripOperationsRecord).mockRejectedValue(new Error('unavailable')); const wrapper = await mount() expect(wrapper.findAll('button').find(b => b.text() === '保存记录')!.attributes('disabled')).toBeDefined() expect(api.saveTripOperationsRecord).not.toHaveBeenCalled() })