fix(fleet): mount measured controls after record loads

This commit is contained in:
2026-10-04 10:07:45 +08:00
parent caf9fd89d4
commit 2c2e19c4ea
2 changed files with 52 additions and 7 deletions
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils'
import { mount as mountFull, shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils'
import { ElForm, ElFormItem, ElInput, ElInputNumber } from 'element-plus'
import TripOperationsRecordDialog from './TripOperationsRecordDialog.vue'
import * as api from '../api/tripOperations'
import { ApiResponseError } from '@/shared/api/http'
@@ -9,16 +10,60 @@ const record: api.TripOperationsRecord = { tripNo: 'TR1', recordVersion: '0', de
const wrappers: VueWrapper[] = []
const slot = { template: '<div><slot/></div>' }
const input = { name: 'ElInputNumber', props: ['modelValue'], emits: ['update:modelValue'], template: '<input :value="modelValue" @input="$emit(\'update:modelValue\',Number($event.target.value))" />' }
async function mount(readonly = false) {
const wrapper = shallowMount(TripOperationsRecordDialog, { props: { modelValue: false, row, readonly }, global: { stubs: {
AppDialog: { props: ['modelValue'], template: '<div v-if="modelValue"><slot/><slot name="footer"/></div>' }, ElForm: slot, ElFormItem: slot,
ElTabs: { name: 'ElTabs', props: ['modelValue'], emits: ['update:modelValue'], template: '<div><slot/></div>' }, ElTabPane: slot, ElInputNumber: input,
ElInput: { ...input, name: 'ElInput' }, ElButton: { props: ['disabled', 'loading'], template: '<button :disabled="disabled || loading"><slot/></button>' },
async function mount(readonly = false, realControls = false) {
const wrapper = (realControls ? mountFull : shallowMount)(TripOperationsRecordDialog, { props: { modelValue: false, row, readonly }, global: { components: { ElForm, ElFormItem, ElInput, ElInputNumber }, stubs: {
AppDialog: { props: ['modelValue'], template: '<div v-if="modelValue"><slot/><slot name="footer"/></div>' }, ElForm: realControls ? false : slot, ElFormItem: realControls ? false : slot,
ElTabs: { name: 'ElTabs', props: ['modelValue'], emits: ['update:modelValue'], template: '<div><slot/></div>' }, ElTabPane: slot, ElInputNumber: realControls ? false : input,
ElInput: realControls ? false : { ...input, name: 'ElInput' }, ElButton: { props: ['disabled', 'loading'], template: '<button :disabled="disabled || loading"><slot/></button>' },
ElAlert: { props: ['title'], template: '<div>{{ title }}<slot/></div>' }, 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.each([[null, 0], [0, null]])('delayed records keep distance %s and fuel %s editable in real number controls', async (distance, fuel) => {
let resolve!: (value: api.TripOperationsRecord) => void
vi.mocked(api.getTripOperationsRecord).mockReturnValue(new Promise(done => { resolve = done }))
const wrapper = await mount(false, true)
expect(wrapper.findComponent(ElForm).exists()).toBe(false)
expect(wrapper.findAll('button').find(button => button.text() === '保存记录')!.attributes('disabled')).toBeDefined()
resolve({ ...record, details: { ...record.details, actualDistanceMiles: distance, actualFuelLitres: fuel } })
await flushPromises()
const controls = wrapper.findAllComponents(ElInputNumber)
expect(controls.map(control => control.props('modelValue'))).toEqual([distance, fuel])
const inputs = controls.map(control => control.find('input'))
expect(inputs.map(input => (input.element as HTMLInputElement).value)).toEqual([distance === null ? '' : '0.0', fuel === null ? '' : '0.0'])
for (const input of inputs) {
expect((input.element as HTMLInputElement).disabled).toBe(false)
expect((input.element as HTMLInputElement).readOnly).toBe(false)
expect(input.attributes('aria-disabled')).toBe('false')
}
await inputs[0]!.setValue('12.5')
await inputs[0]!.trigger('change')
expect(controls[0]!.props('modelValue')).toBe(12.5)
await inputs[0]!.setValue(distance === null ? '' : '0')
await inputs[0]!.trigger('change')
expect(controls[0]!.props('modelValue')).toBe(distance)
vi.mocked(api.saveTripOperationsRecord).mockResolvedValue(record)
await wrapper.findAll('button').find(button => button.text() === '保存记录')!.trigger('click')
await flushPromises()
expect(vi.mocked(api.saveTripOperationsRecord).mock.calls[0]![1].details).toMatchObject({ actualDistanceMiles: distance, actualFuelLitres: fuel })
})
it('delayed read-only records keep real number controls disabled', async () => {
let resolve!: (value: api.TripOperationsRecord) => void
vi.mocked(api.getTripOperationsRecord).mockReturnValue(new Promise(done => { resolve = done }))
const wrapper = await mount(true, true)
expect(wrapper.findComponent(ElForm).exists()).toBe(false)
resolve({ ...record, details: { ...record.details, actualFuelLitres: 0 } })
await flushPromises()
expect(wrapper.findAllComponents(ElInputNumber).map(control => control.props('modelValue'))).toEqual([null, 0])
for (const control of wrapper.findAllComponents(ElInputNumber)) {
const input = control.find('input')
expect((input.element as HTMLInputElement).disabled).toBe(true)
expect(input.attributes('aria-disabled')).toBe('true')
}
expect(wrapper.text()).not.toContain('保存记录')
expect(api.saveTripOperationsRecord).not.toHaveBeenCalled()
})
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()
@@ -85,7 +85,7 @@ watch([tab, page, size], () => { clearPreview(); if (tab.value === 'history') vo
<p v-if="row">{{ row.tripNo }} · {{ row.vehiclePlate || '—' }} · 实际发车 {{ row.departedAt }} · 货物结案 {{ row.completedAt || '—' }}</p>
<el-tabs v-model="tab" class="density-element-tabs density-contained-tabs"><el-tab-pane label="实测记录" name="measured">
<p v-if="uncertain">上次保存结果未确认,将按原请求重试。</p>
<el-form label-position="top" class="density-form record-form" :disabled="readonly || !record || loading || saving || uploading || uncertain">
<el-form v-if="record" label-position="top" class="density-form record-form" :disabled="readonly || !record || loading || saving || uploading || uncertain">
<el-form-item label="本趟实际里程(mile)"><el-input-number :model-value="form.actualDistanceMiles" @update:model-value="form.actualDistanceMiles = $event ?? null" :min="0" :precision="1" placeholder="选填,本趟实际英里数" /></el-form-item>
<el-form-item label="本趟实际油耗(L)"><el-input-number :model-value="form.actualFuelLitres" @update:model-value="form.actualFuelLitres = $event ?? null" :min="0" :precision="1" placeholder="选填,本趟实际用油升数" /></el-form-item>
<el-form-item label="备注" class="full"><el-input :model-value="form.remark || ''" @update:model-value="form.remark = $event || null" type="textarea" maxlength="1000" placeholder="选填,补充实测情况" /></el-form-item>