From 35f286ab7b67a783de75773f5382d431684961c1 Mon Sep 17 00:00:00 2001 From: Viki Date: Tue, 22 Oct 2024 15:52:28 +0800 Subject: [PATCH] test: add more test cases to `useInfiniteList` --- .../src/use-paging-list/index.test.ts | 229 +++++++++++++++++- .../react-use/src/use-paging-list/index.ts | 13 +- .../src/use-request/use-request-cache.ts | 189 --------------- 3 files changed, 228 insertions(+), 203 deletions(-) delete mode 100644 packages/react-use/src/use-request/use-request-cache.ts diff --git a/packages/react-use/src/use-paging-list/index.test.ts b/packages/react-use/src/use-paging-list/index.test.ts index 3e2b3d6..d7aa713 100644 --- a/packages/react-use/src/use-paging-list/index.test.ts +++ b/packages/react-use/src/use-paging-list/index.test.ts @@ -59,9 +59,11 @@ describe('usePagingList', () => { expect(fetcherMock).toHaveBeenCalled() }) - it('should update selection state', () => { + it('should update selection state', async () => { const { result } = renderHook(() => usePagingList({ fetcher: fetcherMock })) + await act(async () => {}) // wait for initial fetch + act(() => { result.current.selection.setSelected([1, 2, 3]) }) @@ -76,9 +78,7 @@ describe('usePagingList', () => { expect(result.current.loading).toBe(true) - await act(async () => { - await fetcherMock() - }) + await act(async () => {}) // wait for initial fetch expect(result.current.loading).toBe(false) }) @@ -96,6 +96,8 @@ describe('usePagingList', () => { }), ) + await act(async () => {}) // wait for initial fetch + expect(fetcherMock).toHaveBeenCalledTimes(1) await act(async () => { @@ -105,4 +107,223 @@ describe('usePagingList', () => { expect(fetcherMock).toHaveBeenCalledTimes(2) expect(onPageSizeChange).toHaveBeenCalledTimes(1) }) + + it('should handle form reset', async () => { + const { result } = renderHook(() => + usePagingList({ fetcher: fetcherMock, form: { initialValue: { name: 'initial' } } }), + ) + + await act(async () => {}) // wait for initial fetch + + await act(async () => { + result.current.form.setValue({ name: 'value' }) + }) + + expect(result.current.form.value).toEqual({ name: 'value' }) + expect(fetcherMock).toHaveBeenCalledTimes(1) + + await act(async () => { + result.current.form.reset() + }) + + expect(result.current.form.value).toEqual({ name: 'initial' }) + expect(fetcherMock).toHaveBeenCalledTimes(2) + }) + + it('should handle page change', async () => { + const { result } = renderHook(() => + usePagingList({ + fetcher: async (params) => { + const res = await fetcherMock() + params.setTotal(100) + return res + }, + pagination: { + pageSize: 20, + }, + }), + ) + + await act(async () => {}) // wait for initial fetch + + await act(async () => { + result.current.pagination.go(2) + }) + + expect(fetcherMock).toHaveBeenCalledTimes(2) + }) + + it('should reserve previous data on page change', async () => { + fetcherMock.mockResolvedValue([{ id: 1 }, { id: 2 }, { id: 3 }]) + + const { result } = renderHook(() => + usePagingList({ + fetcher: async (params) => { + const res = await fetcherMock() + params.setTotal(100) + return res + }, + pagination: { + pageSize: 20, + }, + }), + ) + + await act(async () => {}) // wait for initial fetch + + act(() => { + result.current.selection.select({ id: 1 }) + result.current.selection.select({ id: 2 }) + }) + + expect(result.current.selection.selected).toEqual([{ id: 1 }, { id: 2 }]) + + fetcherMock.mockResolvedValue([{ id: 4 }, { id: 5 }, { id: 6 }]) + + act(() => { + result.current.pagination.next() + }) + + act(() => { + result.current.selection.select({ id: 4 }) + }) + + expect(result.current.selection.selected).toEqual([{ id: 1 }, { id: 2 }, { id: 4 }]) + + fetcherMock.mockResolvedValue([{ id: 4 }, { id: 5 }, { id: 6 }]) + + act(() => { + result.current.pagination.prev() + }) + + expect(result.current.selection.selected).toEqual([{ id: 1 }, { id: 2 }, { id: 4 }]) + }) + + it('should handle onReset', async () => { + const onReset = vi.fn() + + const { result } = renderHook(() => + usePagingList({ + fetcher: fetcherMock, + form: { onReset }, + }), + ) + + await act(async () => {}) // wait for initial fetch + + await act(async () => { + result.current.form.reset() + }) + + expect(onReset).toHaveBeenCalled() + }) + + it('should handle onSubmit', async () => { + const onSubmit = vi.fn() + + const { result } = renderHook(() => + usePagingList({ + fetcher: fetcherMock, + form: { onSubmit }, + }), + ) + + await act(async () => {}) // wait for initial fetch + + await act(async () => { + result.current.form.submit() + }) + + expect(onSubmit).toHaveBeenCalled() + }) + + it('should handle onChange', async () => { + const onChange = vi.fn() + + const { result } = renderHook(() => + usePagingList({ + fetcher: fetcherMock, + form: { onChange, initialValue: { name: 'initial' } }, + }), + ) + + await act(async () => {}) // wait for initial fetch + + await act(async () => { + result.current.form.handleChange({ name: 'newValue' }) + }) + + expect(onChange).toHaveBeenCalled() + }) + + it('should handle onPageChange', async () => { + const onPageChange = vi.fn() + + const { result } = renderHook(() => + usePagingList({ + fetcher: async (params) => { + const res = await fetcherMock() + params.setTotal(100) + return res + }, + pagination: { onPageChange }, + }), + ) + + await act(async () => {}) // wait for initial fetch + + expect(fetcherMock).toHaveBeenCalledTimes(1) + + await act(async () => { + result.current.pagination.go(2) + }) + + await act(async () => {}) + + expect(fetcherMock).toHaveBeenCalledTimes(2) + + expect(onPageChange).toHaveBeenCalledTimes(1) + }) + + it('should handle onBefore', async () => { + const onBefore = vi.fn() + + const { result } = renderHook(() => + usePagingList({ + fetcher: fetcherMock, + query: { onBefore }, + }), + ) + + await act(async () => {}) // wait for initial fetch + + expect(onBefore).toHaveBeenCalledTimes(1) + + await act(async () => { + result.current.form.submit() + }) + + expect(onBefore).toHaveBeenCalledTimes(2) + }) + + it('should handle onSuccess', async () => { + const onSuccess = vi.fn() + + const { result } = renderHook(() => + usePagingList({ + fetcher: fetcherMock, + query: { onSuccess }, + }), + ) + + await act(async () => {}) // wait for initial fetch + + expect(onSuccess).toHaveBeenCalledTimes(1) + + await act(async () => { + result.current.form.submit() + }) + + expect(onSuccess).toHaveBeenCalledTimes(2) + }) }) diff --git a/packages/react-use/src/use-paging-list/index.ts b/packages/react-use/src/use-paging-list/index.ts index 957d2ef..3767992 100644 --- a/packages/react-use/src/use-paging-list/index.ts +++ b/packages/react-use/src/use-paging-list/index.ts @@ -154,21 +154,14 @@ export function usePagingList< }, }) - const startNewQuery = useStableFn((resetPageSize = false) => { + const startNewQuery = useStableFn(() => { previousDataRef.current = undefined paginationActions.go(1) - let pageSize = paginationState.pageSize - - if (resetPageSize) { - paginationActions.setPageSize(options.pagination?.pageSize ?? 10) - pageSize = options.pagination?.pageSize ?? 10 - } - query.run({ previousData: previousDataRef.current, page: 1, - pageSize, + pageSize: paginationState.pageSize, form: form.value, setTotal, }) @@ -194,7 +187,7 @@ export function usePagingList< }, }) - const query = useQuery((options.fetcher ?? (() => {})) as Fetcher, { + const query = useQuery(options?.fetcher as Fetcher, { ...options.query, initialParams: [ { diff --git a/packages/react-use/src/use-request/use-request-cache.ts b/packages/react-use/src/use-request/use-request-cache.ts deleted file mode 100644 index fe546ba..0000000 --- a/packages/react-use/src/use-request/use-request-cache.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { useCreation } from '../use-creation' -import { useEffectOnce } from '../use-effect-once' -import { useLatest } from '../use-latest' -import { useRender } from '../use-render' -import { useStableFn } from '../use-stable-fn' -import { isDefined, isFunction, isNumber } from '../utils/basic' -import { shallowEqual } from '../utils/equal' -import { unwrapArrayable, unwrapGettable } from '../utils/unwrap' - -import type { SetStateAction } from 'react' -import type { AnyFunc, Arrayable, Gettable, SetTimeoutReturn } from '../utils/basic' - -export interface UseRequestCacheLike { - get(key: string): Data | undefined - set(key: string, value: Data): void - delete(key: string): void - keys(): IterableIterator -} - -const dataCache: UseRequestCacheLike = /* #__PURE__ */ new Map() -const paramsCache: Map = /* #__PURE__ */ new Map() -const timerCache: Map = /* #__PURE__ */ new Map() -const promiseCache: Map> = /* #__PURE__ */ new Map() - -const cacheBus = createEventBus() - -export function useRequestCache>>( - options: { - provider?: Gettable> - cacheKey?: string | ((...args: Parameters | []) => string) - cacheExpirationTime?: number | false - compare?: (prevData: D | undefined, nextData: D | undefined) => boolean - } = {}, -) { - const render = useRender() - const provider = unwrapGettable(options.provider) || dataCache - const cacheKeyValue = unwrapGettable(options.cacheKey) - const cachedData = (cacheKeyValue ? provider.get(cacheKeyValue) : undefined) as D | undefined - const cachedParams = (cacheKeyValue ? (paramsCache.get(cacheKeyValue) ?? []) : []) as Parameters | [] - const cacheExpirationTime = options.cacheExpirationTime ?? 5 * 60_000 - - const latest = useLatest({ - provider, - cacheKeyValue, - cacheExpirationTime, - compare: options.compare ?? shallowEqual, - }) - - useEffectOnce(() => { - if (cacheKeyValue) { - return cacheBus.on(cacheKeyValue, render) - } - }) - - function resetCacheTimer() { - const { provider, cacheKeyValue, cacheExpirationTime } = latest.current - - if (!cacheKeyValue || cacheExpirationTime === false) return - - const timer = timerCache.get(cacheKeyValue) - - if (timer) { - clearTimeout(timer) - timerCache.delete(cacheKeyValue) - } - - if (isNumber(cacheExpirationTime)) { - const timer = setTimeout(() => { - provider.delete(cacheKeyValue) - timerCache.delete(cacheKeyValue) - cacheBus.emit(cacheKeyValue) - }, cacheExpirationTime) - - timerCache.set(cacheKeyValue, timer) - } - } - - const setCache = useStableFn((value: D | undefined, params: Parameters | [] = []) => { - const { provider, cacheKeyValue } = latest.current - - if (!cacheKeyValue) return - - const preValue = provider.get(cacheKeyValue) as D | undefined - const preParams = paramsCache.get(cacheKeyValue) as Parameters | [] - - if (shallowEqual(preValue, value) && shallowEqual(preParams, params)) return - - if (!isDefined(value)) { - provider.delete(cacheKeyValue) - timerCache.delete(cacheKeyValue) - } else { - provider.set(cacheKeyValue, value) - resetCacheTimer() - } - - paramsCache.set(cacheKeyValue, params) - - if (!latest.current.compare(preValue, value)) { - cacheBus.emit(cacheKeyValue) - } - }) - - const getPromiseCache = useStableFn(() => { - const { cacheKeyValue } = latest.current - if (!cacheKeyValue) return - return promiseCache.get(cacheKeyValue) - }) - - const setPromiseCache = useStableFn((promise: Promise) => { - const { cacheKeyValue } = latest.current - if (!cacheKeyValue) return - promiseCache.set(cacheKeyValue, promise) - }) - - const clearPromiseCache = useStableFn(() => { - const { cacheKeyValue } = latest.current - if (!cacheKeyValue) return - promiseCache.delete(cacheKeyValue) - }) - - const isCacheEnabled = useStableFn(() => Boolean(latest.current.cacheKeyValue)) - - const actions = useCreation(() => ({ - setCache, - getPromiseCache, - setPromiseCache, - clearPromiseCache, - isCacheEnabled, - })) - - return [{ data: cachedData, params: cachedParams }, actions] as const -} - -export const mutate = /* #__PURE__ */ createMutate(dataCache, paramsCache) - -export type UseRequestMutate = (keyFilter: (key: string) => boolean, value?: unknown, params?: unknown[]) => void - -function createMutate(dataCache: UseRequestCacheLike, paramsCache: Map): UseRequestMutate { - return ( - keyFilter: Arrayable | ((key: string) => boolean), - value: SetStateAction = undefined, - params: unknown[] = [], - ) => { - const keys = isFunction(keyFilter) - ? Array.from(dataCache.keys()).filter(keyFilter) - : unwrapArrayable(keyFilter).filter(Boolean) - - for (const key of keys) { - const prevData = dataCache.get(key) - const nextData = isFunction(value) ? value(prevData) : value - - if (isDefined(key)) { - dataCache.set(key, nextData) - paramsCache.set(key, params) - } else { - dataCache.delete(key) - paramsCache.delete(key) - } - - cacheBus.emit(key) - } - } -} - -function createEventBus() { - const listeners = new Map void>>() - - function on(eventName: string, listener: () => void) { - const set = listeners.get(eventName) || new Set() - set.add(listener) - listeners.set(eventName, set) - return () => off(eventName, listener) - } - - function off(eventName: string, listener: () => void) { - const set = listeners.get(eventName) - if (!set) return - set.delete(listener) - if (set.size === 0) listeners.delete(eventName) - } - - function emit(eventName: string) { - const set = listeners.get(eventName) - if (!set) return - for (const listener of set) listener() - } - - return { on, emit } -}