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[] = [] 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() { let resolve!: (value: T) => void let reject!: (cause: Error) => void const promise = new Promise((yes, no) => { resolve = yes; reject = no }) return { promise, resolve, reject } } function setup() { const selectedId = ref(null) const load = vi.fn<(query: PlatformTenantQuery) => Promise>() 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(), latest = deferred() 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() 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() 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() 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) }) })