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() })