68 lines
5.0 KiB
TypeScript
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)
|
|
})
|