feat(appointment): complete customer receiving and trip continuation workflows
This commit is contained in:
@@ -1,24 +1,60 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import WorkDetailPanel from './WorkDetailPanel.vue'
|
||||
import { getWorkDetail, type WorkAction, type WorkDetail } from './workApi'
|
||||
import { getWorkDetail, resolveBatchWork, type WorkAction, type WorkDetail } from './workApi'
|
||||
import { batchContext } from './batch.test-support'
|
||||
import { followAction, workDetail, workItem } from './work.test-support'
|
||||
vi.mock('./workApi', async original => ({ ...await original<typeof import('./workApi')>(), getWorkDetail: vi.fn() }))
|
||||
vi.mock('./workApi', async original => ({ ...await original<typeof import('./workApi')>(), getWorkDetail: vi.fn(), resolveBatchWork: vi.fn() }))
|
||||
function render(detail: WorkDetail, initialAction?: WorkAction) {
|
||||
vi.mocked(getWorkDetail).mockResolvedValue(detail)
|
||||
return mount(WorkDetailPanel, { props: { itemKey: detail.item.workItemKey, initialAction, canLeave: async () => true }, global: { stubs: {
|
||||
ElButton: { template: '<button><slot /></button>' },
|
||||
ElButton: { props: ['disabled'], template: '<button :disabled="disabled"><slot /></button>' },
|
||||
CargoVerificationForm: { name: 'CargoVerificationForm', props: ['source'], template: '<div data-cargo />' },
|
||||
WorkCommandForm: { name: 'WorkCommandForm', props: ['detail', 'action'], template: '<div data-command />' },
|
||||
WorkAppointmentForm: { name: 'WorkAppointmentForm', props: ['appointmentNo', 'canWrite'], template: '<div data-appointment />' },
|
||||
WorkReceivingPanel: { name: 'WorkReceivingPanel', props: ['batchKey', 'canWrite'], template: '<div data-receiving-scope />' },
|
||||
WorkCreateAppointmentForm: { name: 'WorkCreateAppointmentForm', props: ['sources', 'canWrite'], template: '<div data-create />' },
|
||||
WorkDispatchForm: { name: 'WorkDispatchForm', props: ['batchKeys', 'canWrite'], template: '<div data-dispatch />' },
|
||||
WorkExistingDispatchForm: { name: 'WorkExistingDispatchForm', props: ['dispatches', 'canWrite'], template: '<div data-existing-dispatch />' },
|
||||
WorkReceiptPlans: { name: 'WorkReceiptPlans', props: ['containerBookingId'], template: '<div data-receiving />' },
|
||||
LoadErrorState: { props: ['message'], template: '<p>{{ message }}</p>' }, ApiErrorMeta: true,
|
||||
LoadErrorState: { props: ['message'], template: '<section role="alert">{{ message }}<button @click="$emit(\'retry\')">重试读取</button></section>' }, ApiErrorMeta: true,
|
||||
} } })
|
||||
}
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it('stays on the saved item and offers only the newly fetched primary next step', async () => {
|
||||
const wrapper = render(workDetail(), followAction); await flushPromises()
|
||||
const transport: WorkAction = { code: 'ARRANGE_TRANSPORT', label: '继续安排本批车辆', targetWorkItemKey: 'transport-current', target: { kind: 'FLEET_TRIP', tripId: null, batchKeys: ['FBA_APPOINTMENT:1'] } }
|
||||
vi.mocked(getWorkDetail).mockResolvedValue(workDetail({ item: workItem({ nextAction: transport, allowedActions: [transport, followAction] }) }))
|
||||
wrapper.getComponent({ name: 'WorkCommandForm' }).vm.$emit('saved', { item: workItem(), affectedWorkItemKeys: [] }, 'SAVE_FOLLOWUP'); await flushPromises()
|
||||
expect(wrapper.find('[data-command]').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('本次保存已完成')
|
||||
expect(wrapper.get('.primary-next-action').text()).toBe('继续安排本批车辆')
|
||||
expect(wrapper.findAll('.more-actions button').map(button => button.text())).toEqual(['记录回复'])
|
||||
await wrapper.get('.primary-next-action').trigger('click'); await flushPromises()
|
||||
expect(wrapper.emitted('open')).toEqual([['transport-current', transport]])
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('does not repeat a saved command or enable stale actions when the next-step read fails', async () => {
|
||||
const wrapper = render(workDetail(), followAction); await flushPromises()
|
||||
vi.mocked(getWorkDetail).mockRejectedValueOnce(new Error('下一步服务暂不可用'))
|
||||
wrapper.getComponent({ name: 'WorkCommandForm' }).vm.$emit('saved', { item: workItem(), affectedWorkItemKeys: [] }, 'SAVE_FOLLOWUP'); await flushPromises()
|
||||
expect(wrapper.text()).toContain('无需重复提交已保存内容')
|
||||
expect(wrapper.get('.primary-next-action').attributes('disabled')).toBeDefined()
|
||||
await wrapper.get('[role="alert"] button').trigger('click'); await flushPromises()
|
||||
expect(wrapper.get('.primary-next-action').attributes('disabled')).toBeUndefined()
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('shows an undated job separately from its follow-up date and flags missing follow-up ownership', async () => {
|
||||
const wrapper = render(workDetail()); await flushPromises()
|
||||
expect(wrapper.get('.followup-summary').text()).toContain('2026-10-15')
|
||||
expect(wrapper.get('.followup-summary').text()).toContain('作业日期未定,跟进日不作为作业日期')
|
||||
wrapper.unmount()
|
||||
const missing = render(workDetail({ item: workItem({ owner: null, followupDate: null }) })); await flushPromises()
|
||||
expect(missing.text()).toContain('负责人待分派')
|
||||
expect(missing.text()).toContain('请补充下一次跟进安排')
|
||||
missing.unmount()
|
||||
})
|
||||
it('never sends customer-warehouse dispatch batches to the FBA candidate endpoint', async () => {
|
||||
const action: WorkAction = { code: 'ARRANGE_TRANSPORT', label: '安排运输', targetWorkItemKey: null, target: { kind: 'FLEET_TRIP', tripId: null, batchKeys: ['DISPATCH:9007199254740999'] } }
|
||||
const wrapper = render(workDetail({ item: workItem({ allowedActions: [action], nextAction: action }) }), action); await flushPromises()
|
||||
@@ -81,6 +117,41 @@ it('uses the matched appointment business number and never passes the internal a
|
||||
expect(missing.findComponent({ name: 'WorkAppointmentForm' }).exists()).toBe(false)
|
||||
expect(missing.text()).toContain('尚未关联可操作的预约记录'); missing.unmount()
|
||||
})
|
||||
it('opens the receiving scope for the exact batch without changing the current work identity or losing result context', async () => {
|
||||
const action: WorkAction = { code: 'CHANGE_BOOKING', label: '调整预约', targetWorkItemKey: null, target: { kind: 'FBA_APPOINTMENT', appointmentId: 'internal-id', batchKey: 'FBA_APPOINTMENT:1', sources: [] } }
|
||||
const wrapper = render(workDetail({ item: workItem({ allowedActions: [action], nextAction: action }), references: [{ kind: 'FBA_APPOINTMENT', key: 'FBA_APPOINTMENT:1', businessNo: 'AP-TEST', label: '预约' }] }), action)
|
||||
await flushPromises(); wrapper.getComponent({ name: 'WorkAppointmentForm' }).vm.$emit('receiving', 'FBA_APPOINTMENT:1'); await flushPromises()
|
||||
const scope = wrapper.getComponent({ name: 'WorkReceivingPanel' })
|
||||
expect(scope.props('batchKey')).toBe('FBA_APPOINTMENT:1')
|
||||
scope.vm.$emit('updated'); await flushPromises()
|
||||
expect(wrapper.emitted('updated')).toHaveLength(1)
|
||||
expect(getWorkDetail).toHaveBeenCalledTimes(2)
|
||||
expect(getWorkDetail).toHaveBeenLastCalledWith('work-delivery-1', expect.any(AbortSignal))
|
||||
expect(wrapper.findComponent({ name: 'WorkReceivingPanel' }).exists()).toBe(true)
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
scope.vm.$emit('appointment', 'AP-LATER'); await flushPromises()
|
||||
expect(wrapper.getComponent({ name: 'WorkAppointmentForm' }).props('appointmentNo')).toBe('AP-LATER')
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('resolves the saved later batch and offers its real vehicle next action instead of refreshing the historical work', async () => {
|
||||
const edit: WorkAction = { code: 'CHANGE_BOOKING', label: '调整预约', targetWorkItemKey: null, target: { kind: 'FBA_APPOINTMENT', appointmentId: '1', batchKey: 'FBA_APPOINTMENT:1', sources: [] } }
|
||||
const original = workDetail({ item: workItem({ allowedActions: [edit], nextAction: edit }), references: [{ kind: 'FBA_APPOINTMENT', key: 'FBA_APPOINTMENT:1', businessNo: 'AP-OLD', label: '预约' }] })
|
||||
const wrapper = render(original, edit); await flushPromises()
|
||||
wrapper.getComponent({ name: 'WorkAppointmentForm' }).vm.$emit('receiving', 'FBA_APPOINTMENT:1'); await flushPromises()
|
||||
wrapper.getComponent({ name: 'WorkReceivingPanel' }).vm.$emit('appointment', 'AP-LATER-40'); await flushPromises()
|
||||
const transport: WorkAction = { code: 'ARRANGE_TRANSPORT', label: '继续安排本批车辆', targetWorkItemKey: 'real-transport-later', target: { kind: 'FLEET_TRIP', tripId: null, batchKeys: ['FBA_APPOINTMENT:40'] } }
|
||||
const childDetail = workDetail({ item: workItem({ workItemKey: 'real-work-child', batchKey: 'FBA_APPOINTMENT:40', nextAction: transport, allowedActions: [transport] }) })
|
||||
vi.mocked(resolveBatchWork).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:40', customerId: 'customer-a', detail: childDetail, candidates: [] })
|
||||
wrapper.getComponent({ name: 'WorkAppointmentForm' }).vm.$emit('saved', batchContext('AP-LATER-40', 'FBA_APPOINTMENT:40').appointment); await flushPromises()
|
||||
expect(resolveBatchWork).toHaveBeenCalledWith('FBA_APPOINTMENT:40', undefined, expect.any(AbortSignal))
|
||||
expect(getWorkDetail).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
expect(wrapper.emitted('open')).toEqual([['real-work-child']]); wrapper.unmount()
|
||||
const current = render(childDetail); await flushPromises()
|
||||
expect(current.get('.primary-next-action').text()).toBe('继续安排本批车辆')
|
||||
await current.get('.primary-next-action').trigger('click'); await flushPromises()
|
||||
expect(current.emitted('open')).toEqual([['real-transport-later', transport]]); current.unmount()
|
||||
})
|
||||
it('displays all associated containers of a shared trip without creating duplicate transport work', async () => {
|
||||
const wrapper = render(workDetail({ item: workItem({ type: 'TRANSPORT', containers: [{ containerBookingId: '1', bookingNo: 'D-1', containerNo: 'TEST-CN-A' }, { containerBookingId: '2', bookingNo: 'D-2', containerNo: 'TEST-CN-B' }] }) })); await flushPromises()
|
||||
expect(wrapper.findAll('.container-links button')).toHaveLength(2)
|
||||
|
||||
Reference in New Issue
Block a user