import { ref } from 'vue' import { beforeEach, expect, it, vi } from 'vitest' import type { ApiErrorDetails } from '@/shared/api/http' import { listDispatchCandidates } from './api/dispatches' import { candidate, sharedPalletCandidates } from './dispatchTestFixtures' import type { CandidateList } from './dispatchContracts' import { useDispatchCandidates } from './useDispatchCandidates' vi.mock('./api/dispatches', () => ({ listDispatchCandidates: vi.fn() })) beforeEach(() => vi.resetAllMocks()) const response = (overrides: Partial = {}): CandidateList => ({ candidates: [candidate()], snapshotAt: '', complete: false, page: 1, size: 20, total: 41, pages: 3, ...overrides }) function deferred() { let resolve!: (value: CandidateList) => void, reject!: (error: Error) => void const promise = new Promise((yes, no) => { resolve = yes; reject = no }) return { promise, resolve, reject } } function setup() { const error = ref(null) return { ...useDispatchCandidates(error), error } } it('deduplicates concurrent identical requests and accepts complete groups on a partial page', async () => { const pending = deferred(), state = setup() vi.mocked(listDispatchCandidates).mockReturnValue(pending.promise) const first = state.load({ warehouseId: '321', page: 2, warehouseTab: 'LAX', orderNo: 'LM' }) const second = state.load({ warehouseId: '321', page: 2, warehouseTab: 'LAX', orderNo: 'LM' }) expect(first).toBe(second); expect(listDispatchCandidates).toHaveBeenCalledTimes(1) pending.resolve(response({ candidates: [sharedPalletCandidates()[0]!], page: 2 })) await first expect(state.total.value).toBe(41); expect(state.page.value).toBe(2) expect(state.candidates.value[0]!.selectionGroup.sources).toHaveLength(2) expect(() => state.assertCurrent(state.candidates.value)).not.toThrow() }) it.each(['success', 'failure'])('ignores stale %s while the latest page is still loading', async outcome => { const old = deferred(), latest = deferred(), state = setup() vi.mocked(listDispatchCandidates).mockReturnValueOnce(old.promise).mockReturnValueOnce(latest.promise) const first = state.load({ warehouseId: '321', page: 1 }) const second = state.load({ warehouseId: '321', page: 2 }) if (outcome === 'success') old.resolve(response()); else old.reject(new Error('stale failure')) await first expect(state.loading.value).toBe(true); expect(state.error.value).toBeNull(); expect(state.candidates.value).toEqual([]) latest.resolve(response({ page: 2 })); await second expect(state.page.value).toBe(2); expect(state.loading.value).toBe(false) }) it('ignores an old response after a new warehouse succeeds and invalidates requests on close', async () => { const old = deferred(), state = setup(), other = candidate({ warehouseId: 'other' }) vi.mocked(listDispatchCandidates).mockReturnValueOnce(old.promise).mockResolvedValueOnce(response({ candidates: [other] })) const first = state.load({ warehouseId: '321' }) await state.load({ warehouseId: 'other' }) old.resolve(response()); await first expect(state.candidates.value[0]!.warehouseId).toBe('other') expect(() => state.assertCurrent([candidate()])).toThrow('组成已变化') const closing = deferred() vi.mocked(listDispatchCandidates).mockReturnValueOnce(closing.promise) const request = state.load({ warehouseId: 'other', page: 2 }) state.reset(); closing.resolve(response({ page: 2, candidates: [other] })); await request expect(state.candidates.value).toEqual([]); expect(state.total.value).toBe(0); expect(state.loading.value).toBe(false) }) it('remembers a group or source version change even after navigating to an unrelated page', async () => { const state = setup(), selected = candidate() const changed = candidate({ selectionGroup: { ...selected.selectionGroup, groupVersion: 'changed' } }) vi.mocked(listDispatchCandidates).mockResolvedValueOnce(response()).mockResolvedValueOnce(response({ page: 2, candidates: [changed] })).mockResolvedValueOnce(response({ page: 3, candidates: [] })) await state.load({ warehouseId: '321' }); await state.load({ warehouseId: '321', page: 2 }); await state.load({ warehouseId: '321', page: 3 }) expect(() => state.assertCurrent([selected])).toThrow('组成已变化') expect(() => state.assertCurrent([changed])).not.toThrow() }) it.each(['warehouse', 'page', 'version'])('rejects a mismatched %s response instead of exposing selectable stale rows', async mismatch => { const state = setup(), row = candidate() if (mismatch === 'warehouse') row.warehouseId = 'other' if (mismatch === 'version') row.sourceVersion = 'changed' vi.mocked(listDispatchCandidates).mockResolvedValue(response({ candidates: [row], page: mismatch === 'page' ? 2 : 1 })) await state.load({ warehouseId: '321' }) expect(state.candidates.value).toEqual([]); expect(state.error.value).not.toBeNull() }) it.each(['success', 'failure'])('keeps rows stable during a refresh but rejects their use until %s is resolved', async outcome => { const state = setup(), refresh = deferred() vi.mocked(listDispatchCandidates).mockResolvedValueOnce(response()).mockReturnValueOnce(refresh.promise) await state.load({ warehouseId: '321' }) const previous = state.candidates.value const request = state.load({ warehouseId: '321' }) expect(state.candidates.value).toBe(previous) expect(state.total.value).toBe(41) expect(() => state.assertCurrent(previous)).toThrow('尚未读取完成') if (outcome === 'success') refresh.resolve(response({ total: 1 })) else refresh.reject(new Error('refresh failed')) await request if (outcome === 'success') { expect(state.total.value).toBe(1) expect(() => state.assertCurrent(state.candidates.value)).not.toThrow() } else { expect(state.candidates.value).toEqual([]) expect(state.error.value?.message).toBeTruthy() expect(() => state.assertCurrent(previous)).toThrow('尚未读取完成') } }) it('allows mixed-warehouse pages only for the explicitly regional workspace and pins restored drafts to their warehouse', async () => { const error = ref(null) const state = useDispatchCandidates(error, listDispatchCandidates, true) const second = candidate({ sourceKey: 'ny-source', warehouseId: '654' }) vi.mocked(listDispatchCandidates).mockResolvedValueOnce(response({ candidates: [candidate(), second] })) await state.load({ warehouseTab: 'ALL' }) expect(state.candidates.value).toHaveLength(2) expect(() => state.assertCurrent([second])).not.toThrow() vi.mocked(listDispatchCandidates).mockResolvedValueOnce(response({ candidates: [candidate()] })) await state.load({ warehouseId: '321', warehouseTab: 'ALL', draftToken: 'server-token' }) expect(() => state.assertCurrent([second])).toThrow('组成已变化') const scoped = setup() await scoped.load({ warehouseTab: 'ALL' }) expect(listDispatchCandidates).toHaveBeenCalledTimes(2) })