110 lines
9.1 KiB
TypeScript
110 lines
9.1 KiB
TypeScript
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { beforeEach, expect, it, vi } from 'vitest'
|
|
import WorkCommandForm from './WorkCommandForm.vue'
|
|
import { submitWorkCommand, type WorkAction } from './workApi'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import { followAction, workDetail, workItem } from './work.test-support'
|
|
import type { LeaveGuard } from './useWorkDraft'
|
|
const session = vi.hoisted(() => ({ canWrite: true }))
|
|
vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: { id: 'user-1', name: '测试调度' } }) }))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => session.canWrite }) }))
|
|
vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn() }, ElMessageBox: { confirm: vi.fn() } }))
|
|
vi.mock('./workApi', async original => ({ ...await original<typeof import('./workApi')>(), submitWorkCommand: vi.fn() }))
|
|
let guard: LeaveGuard
|
|
function render(action: WorkAction = followAction, detail = workDetail({ item: workItem({ allowedActions: [action], nextAction: action }) })) {
|
|
return mount(WorkCommandForm, { props: { detail, action, refreshing: false }, global: { provide: { 'appointment-work-leave-guard': (value: LeaveGuard) => { guard = value; return () => undefined } }, stubs: {
|
|
ElButton: { props: ['nativeType', 'disabled'], template: '<button :type="nativeType" :disabled="disabled"><slot /></button>' },
|
|
LoadErrorState: { props: ['message'], template: '<div role="alert">{{ message }}</div>' }, ApiErrorMeta: true,
|
|
} } })
|
|
}
|
|
beforeEach(() => { vi.clearAllMocks(); session.canWrite = true; vi.mocked(submitWorkCommand).mockResolvedValue({ item: workItem(), affectedWorkItemKeys: [] }) })
|
|
it('sends only follow-up ownership and feedback, retaining the undated work and the exact retry command', async () => {
|
|
vi.mocked(submitWorkCommand).mockRejectedValueOnce(new Error('connection lost'))
|
|
const wrapper = render()
|
|
await wrapper.get('textarea').setValue('已联系,等待回复'); await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
const first = vi.mocked(submitWorkCommand).mock.calls[0]![1]
|
|
expect(first).toMatchObject({ command: 'SAVE_FOLLOWUP', expectedVersion: 3, payload: { ownerId: 'user-1', note: '已联系,等待回复', followupDate: '2026-10-15' } })
|
|
expect(first.payload).not.toHaveProperty('target'); expect(await guard()).toBe(false)
|
|
expect(wrapper.get('fieldset').attributes('disabled')).toBeDefined()
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
expect(submitWorkCommand).toHaveBeenNthCalledWith(2, 'work-delivery-1', first)
|
|
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
|
|
})
|
|
it('preserves entered feedback after a conflict and uses the newly read version on an explicit resubmission', async () => {
|
|
vi.mocked(submitWorkCommand).mockRejectedValueOnce(new ApiResponseError('版本已变化', 'A0409', 409))
|
|
const wrapper = render(); await wrapper.get('textarea').setValue('保留此反馈')
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
const first = vi.mocked(submitWorkCommand).mock.calls[0]![1]
|
|
expect(wrapper.emitted('refresh')).toHaveLength(1)
|
|
await wrapper.setProps({ detail: workDetail({ item: workItem({ version: 4 }) }) })
|
|
expect((wrapper.get('textarea').element as HTMLTextAreaElement).value).toBe('保留此反馈')
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
const second = vi.mocked(submitWorkCommand).mock.calls[1]![1]
|
|
expect(second.expectedVersion).toBe(4); expect(second.idempotencyKey).not.toBe(first.idempotencyKey)
|
|
wrapper.unmount()
|
|
})
|
|
it('uses the distinct review version and never substitutes the item version or marks work complete', async () => {
|
|
const action: WorkAction = { code: 'RESOLVE_REVIEW', label: '复核安排', targetWorkItemKey: null, target: { kind: 'WORK_COMMAND', command: 'RESOLVE_REVIEW' } }
|
|
const wrapper = render(action, workDetail({ item: workItem({ allowedActions: [action], reviewRequired: true }), review: { version: 12, reasons: ['到港日期调整'] } }))
|
|
await wrapper.get('textarea').setValue('已核对原安排'); await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
expect(submitWorkCommand).toHaveBeenCalledWith('work-delivery-1', expect.objectContaining({ expectedVersion: 3, command: 'RESOLVE_REVIEW', payload: { reviewVersion: 12, note: '已核对原安排' } }))
|
|
wrapper.unmount()
|
|
})
|
|
it('requires receiving instructions for a customer warehouse that needs no appointment and does not invent a time', async () => {
|
|
const action: WorkAction = { code: 'SAVE_REQUIREMENT', label: '登记收货要求', targetWorkItemKey: null, target: { kind: 'WORK_COMMAND', command: 'SAVE_REQUIREMENT' } }
|
|
const wrapper = render(action)
|
|
await wrapper.get('input[type=checkbox]').setValue(false)
|
|
await wrapper.findAll('textarea')[1]!.setValue('客户仓确认无需预约')
|
|
await wrapper.get('form').trigger('submit'); expect(submitWorkCommand).not.toHaveBeenCalled()
|
|
await wrapper.findAll('textarea')[0]!.setValue('工作日9至16点到仓联系收货人')
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
expect(submitWorkCommand).toHaveBeenCalledWith('work-delivery-1', expect.objectContaining({ command: 'SAVE_REQUIREMENT', payload: { target: null, bookingRequired: false, receivingRequirements: '工作日9至16点到仓联系收货人', reason: '客户仓确认无需预约' } }))
|
|
wrapper.unmount()
|
|
})
|
|
it('keeps a return handoff separate from original carrier plans and records external acceptance evidence', async () => {
|
|
const action: WorkAction = { code: 'ACCEPT_RETURN', label: '记录接手反馈', targetWorkItemKey: null, target: { kind: 'WORK_COMMAND', command: 'ACCEPT_RETURN' } }
|
|
const wrapper = render(action, workDetail({ item: workItem({ type: 'EMPTY_RETURN', allowedActions: [action] }), returnHandoff: { version: 7, from: { kind: 'USER', id: 'user-1', name: '测试调度' }, to: { kind: 'EXTERNAL', id: null, name: '测试还空车队' }, status: 'PENDING_ACCEPTANCE', reason: '原车次调整' } }))
|
|
await wrapper.get('form').trigger('submit'); expect(submitWorkCommand).not.toHaveBeenCalled()
|
|
await wrapper.get('textarea').setValue('接手车队已回复确认'); await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
expect(submitWorkCommand).toHaveBeenCalledWith('work-delivery-1', expect.objectContaining({ command: 'ACCEPT_RETURN', payload: { handoffVersion: 7, note: '接手车队已回复确认' } }))
|
|
wrapper.unmount()
|
|
})
|
|
it('denies command submission without write permission even when a submit event is forced', async () => {
|
|
session.canWrite = false; const wrapper = render()
|
|
await wrapper.get('form').trigger('submit'); expect(submitWorkCommand).not.toHaveBeenCalled()
|
|
expect(wrapper.text()).toContain('当前仅可查看'); wrapper.unmount()
|
|
})
|
|
|
|
const requirementAction: WorkAction = { code: 'SAVE_REQUIREMENT', label: '登记收货要求', targetWorkItemKey: null, target: { kind: 'WORK_COMMAND', command: 'SAVE_REQUIREMENT' } }
|
|
it('preserves an explicit cross-day local interval in the requirement request', async () => {
|
|
const wrapper = render(requirementAction)
|
|
await wrapper.findAll('textarea')[1]!.setValue('跨日收货窗口')
|
|
const dates = wrapper.findAll('input[type=date]'), times = wrapper.findAll('input[type=time]')
|
|
await dates[0]!.setValue('2026-10-15'); await times[0]!.setValue('23:00')
|
|
await dates[1]!.setValue('2026-10-16'); await times[1]!.setValue('01:00')
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
expect(submitWorkCommand).toHaveBeenCalledWith('work-delivery-1', expect.objectContaining({ payload: expect.objectContaining({ target: { date: '2026-10-15', startTime: '23:00', endDate: '2026-10-16', endTime: '01:00', timezone: 'America/Los_Angeles' } }) }))
|
|
wrapper.unmount()
|
|
})
|
|
it.each([
|
|
['2026-10-16', '', '23:00'], ['', '01:00', '23:00'],
|
|
['2026-10-15', '22:00', '23:00'], ['2026-10-15', '23:00', '23:00'],
|
|
['2026-10-16', '01:00', ''],
|
|
])('rejects incomplete or non-increasing interval %s %s %s without inferring a next day', async (endDate, endTime, startTime) => {
|
|
const wrapper = render(requirementAction)
|
|
await wrapper.findAll('textarea')[1]!.setValue('核对时间')
|
|
const dates = wrapper.findAll('input[type=date]'), times = wrapper.findAll('input[type=time]')
|
|
await dates[0]!.setValue('2026-10-15'); await times[0]!.setValue(startTime)
|
|
await dates[1]!.setValue(endDate); await times[1]!.setValue(endTime)
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
expect(submitWorkCommand).not.toHaveBeenCalled(); wrapper.unmount()
|
|
})
|
|
it('saves a date-only requirement with all time fields null', async () => {
|
|
const wrapper = render(requirementAction)
|
|
await wrapper.findAll('textarea')[1]!.setValue('具体时段待回复')
|
|
await wrapper.findAll('input[type=date]')[0]!.setValue('2026-10-15')
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
expect(submitWorkCommand).toHaveBeenCalledWith('work-delivery-1', expect.objectContaining({ payload: expect.objectContaining({ target: { date: '2026-10-15', startTime: null, endDate: null, endTime: null, timezone: 'America/Los_Angeles' } }) }))
|
|
wrapper.unmount()
|
|
})
|