Files
admin-web/src/modules/overseas-warehouse/useDispatchCandidates.test.ts
T
2026-10-21 16:40:10 +08:00

112 lines
6.8 KiB
TypeScript

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> = {}): 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<CandidateList>((yes, no) => { resolve = yes; reject = no })
return { promise, resolve, reject }
}
function setup() {
const error = ref<ApiErrorDetails | null>(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<ApiErrorDetails | null>(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)
})