112 lines
6.8 KiB
TypeScript
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)
|
|
})
|