Files
admin-web/src/modules/iam/useActiveCustomerOptions.test.ts
T
2026-10-20 23:34:11 +08:00

119 lines
6.5 KiB
TypeScript

import { effectScope, ref } from 'vue'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useActiveCustomerOptions } from './useActiveCustomerOptions'
import type { PlatformTenantPage, PlatformTenantQuery, PlatformTenantResource } from './api/platformIam'
const scopes: ReturnType<typeof effectScope>[] = []
afterEach(() => { scopes.splice(0).forEach(scope => scope.stop()) })
function customer(id: string, tenantName = `客户${id}`, status = 'ACTIVE') {
return { id, tenantName, status } as PlatformTenantResource
}
function page(records: PlatformTenantResource[], current = 1, pages = 1): PlatformTenantPage {
return { records, page: current, size: 50, pages, total: pages * 50 }
}
function deferred<T>() {
let resolve!: (value: T) => void
let reject!: (cause: Error) => void
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no })
return { promise, resolve, reject }
}
function setup() {
const selectedId = ref<string | null>(null)
const load = vi.fn<(query: PlatformTenantQuery) => Promise<PlatformTenantPage>>()
const scope = effectScope(); scopes.push(scope)
const options = scope.run(() => useActiveCustomerOptions(() => selectedId.value, load))!
return { ...options, load, selectedId, scope }
}
describe('active customer remote options', () => {
it('searches the complete customer directory by keyword instead of filtering the first page', async () => {
const view = setup()
view.load.mockResolvedValueOnce(page([customer('1', '跨客户车次-测试客户')], 1, 10))
.mockResolvedValueOnce(page([customer('9007199254740999', '柏悦物流')]))
await view.search()
await view.search(' 柏悦 ')
expect(view.load.mock.calls.map(([query]) => query)).toEqual([
{ page: 1, size: 50, status: 'ACTIVE' },
{ page: 1, size: 50, status: 'ACTIVE', keyword: '柏悦' },
])
expect(view.customers.value.map(row => row.tenantName)).toEqual(['柏悦物流'])
expect(view.customers.value[0]!.id).toBe('9007199254740999')
})
it('continues beyond 200 customers, deduplicates pages, and stops at the final page', async () => {
const view = setup()
view.load.mockImplementation(async query => page([customer('shared'), customer(String(query.page))], query.page, 5))
await view.search('物流')
for (let index = 0; index < 6; index++) await view.endReached('bottom')
expect(view.load.mock.calls.map(([query]) => query.page)).toEqual([1, 2, 3, 4, 5])
expect(view.load.mock.calls.every(([query]) => query.keyword === '物流')).toBe(true)
expect(view.customers.value.map(row => row.id)).toEqual(['shared', '1', '2', '3', '4', '5'])
})
it('ignores late pages and errors from an earlier keyword', async () => {
const view = setup(), old = deferred<PlatformTenantPage>(), latest = deferred<PlatformTenantPage>()
view.load.mockResolvedValueOnce(page([customer('1')], 1, 2)).mockReturnValueOnce(old.promise).mockReturnValueOnce(latest.promise)
await view.search('旧')
const oldRequest = view.endReached('bottom')
const current = view.search('柏悦')
old.reject(new Error('旧查询失败')); await oldRequest
expect(view.loading.value).toBe(true); expect(view.error.value).toBeNull()
latest.resolve(page([customer('2', '柏悦物流')])); await current
expect(view.customers.value.map(row => row.tenantName)).toEqual(['柏悦物流'])
expect(view.loading.value).toBe(false)
})
it('does not let a late successful search overwrite a newer result', async () => {
const view = setup(), old = deferred<PlatformTenantPage>()
view.load.mockReturnValueOnce(old.promise).mockResolvedValueOnce(page([customer('2', '柏悦物流')]))
const first = view.search('旧'); await view.search('柏悦')
old.resolve(page([customer('1', '旧客户')])); await first
expect(view.customers.value.map(row => row.tenantName)).toEqual(['柏悦物流'])
})
it('retains selection and loaded pages after an append failure and retries that page once', async () => {
const view = setup(), next = deferred<PlatformTenantPage>()
view.load.mockResolvedValueOnce(page([customer('1', '已选客户')], 1, 2)).mockReturnValueOnce(next.promise)
.mockResolvedValueOnce(page([customer('2')], 2, 2))
await view.search(); view.selectedId.value = '1'
const pending = view.endReached('bottom'); await view.endReached('bottom')
expect(view.load).toHaveBeenCalledTimes(2)
next.reject(new Error('客户目录暂不可用')); await pending
expect(view.error.value?.message).toContain('客户选项加载失败')
expect(view.customers.value.map(row => row.id)).toEqual(['1'])
await view.endReached('bottom'); expect(view.load).toHaveBeenCalledTimes(2)
await view.retry()
expect(view.load.mock.calls.map(([query]) => query.page)).toEqual([1, 2, 2])
expect(view.selectedId.value).toBe('1'); expect(view.error.value).toBeNull()
expect(view.customers.value.map(row => row.id)).toEqual(['1', '2'])
})
it('keeps the selected label across searches and clears it when selection is cleared', async () => {
const view = setup()
view.load.mockResolvedValueOnce(page([customer('1', '柏悦物流')])).mockResolvedValueOnce(page([customer('2')]))
await view.search('柏悦'); view.selectedId.value = '1'; await view.search('其他')
expect(view.customers.value[0]!.tenantName).toBe('柏悦物流')
view.selectedId.value = null
expect(view.customers.value.map(row => row.id)).toEqual(['2'])
})
it('shows initial errors, retries the same keyword, and excludes inactive customers', async () => {
const view = setup()
view.load.mockRejectedValueOnce(new Error('加载失败')).mockResolvedValueOnce(page([customer('1'), customer('2', '停用客户', 'DISABLED')]))
await view.search('客户'); expect(view.error.value).not.toBeNull()
await view.retry()
expect(view.load.mock.calls[1]![0]).toEqual({ page: 1, size: 50, status: 'ACTIVE', keyword: '客户' })
expect(view.customers.value.map(row => row.id)).toEqual(['1'])
})
it('ignores disposal and non-bottom scroll events', async () => {
const view = setup(), pending = deferred<PlatformTenantPage>()
view.load.mockReturnValueOnce(pending.promise)
await view.endReached('top'); expect(view.load).not.toHaveBeenCalled()
const request = view.search('柏悦'); view.scope.stop()
pending.resolve(page([customer('1')])); await request
expect(view.customers.value).toEqual([])
await view.search('其他'); await view.retry(); expect(view.load).toHaveBeenCalledTimes(1)
})
})