Files
admin-web/src/modules/overseas-warehouse/dispatchCommandRecovery.test.ts
T

68 lines
5.0 KiB
TypeScript

import { beforeEach, expect, it, vi } from 'vitest'
import { ApiResponseError } from '@/shared/api/http'
import { useDispatchCommandRecovery } from './dispatchCommandRecovery'
import { command } from './dispatchTestFixtures'
beforeEach(() => sessionStorage.clear())
it.each([new Error('network unknown'), new ApiResponseError('server failure','S0500',200), new ApiResponseError('gateway timeout','HTTP_504',504), new ApiResponseError('version changed','A0409',200)])('releases failed $message and permits a new request with edited input', async failure => {
const owner=crypto.randomUUID(), state=useDispatchCommandRecovery(()=>owner)
state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'first',remark:'original'})
await expect(state.execute(vi.fn().mockRejectedValue(failure),vi.fn())).rejects.toBe(failure)
expect(state.pending.value).toBeNull();expect(state.busy.value).toBe(false)
expect(sessionStorage.getItem(`dispatch-command:${owner}`)).toBeNull()
const reopened=useDispatchCommandRecovery(()=>owner)
reopened.begin('PC/1','EDIT',{recordVersion:2,idempotencyKey:'second',remark:'edited'})
const send=vi.fn().mockResolvedValue({...command('EDIT','second'),recordVersion:3})
await reopened.execute(send,vi.fn().mockResolvedValue({dispatchNo:'PC/1',recordVersion:3}))
expect(send.mock.calls[0]![0].body).toEqual({recordVersion:2,idempotencyKey:'second',remark:'edited'})
expect(reopened.pending.value).toBeNull()
})
it.each(['{broken',JSON.stringify({attempted:true,result:null,action:'EDIT'})])('clears saved failed requests without restoring them',saved=>{
const owner=crypto.randomUUID()
sessionStorage.setItem(`dispatch-command:${owner}`,saved)
const state=useDispatchCommandRecovery(()=>owner)
expect(state.pending.value).toBeNull();expect(state.busy.value).toBe(false)
expect(sessionStorage.getItem(`dispatch-command:${owner}`)).toBeNull()
})
it('prevents double clicks only during the active request',async()=>{
const owner=crypto.randomUUID(),state=useDispatchCommandRecovery(()=>owner)
state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'first'})
let finish!:(result:ReturnType<typeof command>)=>void
const send=vi.fn().mockReturnValue(new Promise(done=>{finish=done}))
const read=vi.fn().mockResolvedValue({dispatchNo:'PC/1',recordVersion:1})
const running=state.execute(send,read),another=useDispatchCommandRecovery(()=>owner)
expect(another.busy.value).toBe(true)
expect(()=>another.begin('PC/2','EDIT',{recordVersion:0,idempotencyKey:'second'})).toThrow('正在提交')
await another.execute(send,read);expect(send).toHaveBeenCalledTimes(1)
finish(command('EDIT','first'));await running
expect(another.pending.value).toBeNull();expect(another.busy.value).toBe(false)
expect(()=>another.begin('PC/2','EDIT',{recordVersion:0,idempotencyKey:'second'})).not.toThrow()
})
it('unlocks even when a successful write is followed by a failed detail read',async()=>{
const owner=crypto.randomUUID(),state=useDispatchCommandRecovery(()=>owner)
state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'key'})
await expect(state.execute(vi.fn().mockResolvedValue(command('EDIT')),vi.fn().mockRejectedValue(new Error('read failed')))).rejects.toThrow('read failed')
expect(state.pending.value).toBeNull();expect(state.busy.value).toBe(false)
})
it('keeps loading-stage and response identity checks without retaining failed requests',async()=>{
const owner=crypto.randomUUID(),state=useDispatchCommandRecovery(()=>owner),read=vi.fn()
state.begin('PC/1','LOADING_COMPLETE',{recordVersion:0,idempotencyKey:'key'})
await expect(state.execute(vi.fn().mockResolvedValue({...command('LOADING_COMPLETE'),stage:'LOADING'}),read)).rejects.toThrow('装车完成')
expect(read).not.toHaveBeenCalled();expect(state.pending.value).toBeNull()
state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'key'})
await expect(state.execute(vi.fn().mockResolvedValue(command('EDIT','different')),read)).rejects.toThrow('不一致')
expect(read).not.toHaveBeenCalled();expect(state.pending.value).toBeNull()
})
it('preserves large version precision and unlocks after a mismatched detail',async()=>{
const owner=crypto.randomUUID(),state=useDispatchCommandRecovery(()=>owner)
state.begin('PC/1','EDIT',{recordVersion:'9007199254740992',idempotencyKey:'key'})
await expect(state.execute(vi.fn().mockResolvedValue({...command('EDIT'),recordVersion:'9007199254740993'}),vi.fn().mockResolvedValue({dispatchNo:'PC/1',recordVersion:'9007199254740992'}))).rejects.toThrow('详情版本早于')
expect(state.pending.value).toBeNull()
})
it('rejects an account change before sending and releases state',async()=>{
let owner=crypto.randomUUID();const state=useDispatchCommandRecovery(()=>owner)
state.begin('PC/1','EDIT',{recordVersion:0,idempotencyKey:'key'});owner=crypto.randomUUID()
const send=vi.fn()
await expect(state.execute(send,vi.fn())).rejects.toThrow('账号已变化')
expect(send).not.toHaveBeenCalled();expect(state.pending.value).toBeNull();expect(state.busy.value).toBe(false)
})