import React from 'react' import {renderHook, waitFor, act} from '@testing-library/react' import {describe, expect, it, vi} from 'vitest' import {MinisQueryProvider} from './MinisQueryProvider' import {useShopActionInfiniteQuery} from './useShopActionInfiniteQuery' describe('useShopActionInfiniteQuery', () => { const wrapper = ({children}: {children: React.ReactNode}) => ( {children} ) describe('Data Fetching', () => { it('fetches and returns flattened array data', async () => { const mockAction = vi.fn().mockResolvedValue({ ok: true, data: { data: [{id: '1'}, {id: '2'}], pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-array'], mockAction, {}), {wrapper} ) await waitFor(() => { expect(result.current.loading).toBe(false) }) expect(result.current.data).toEqual([{id: '1'}, {id: '2'}]) expect(result.current.hasNextPage).toBe(false) expect(mockAction).toHaveBeenCalledWith({}) }) it('handles non-array data by returning it directly', async () => { const mockAction = vi.fn().mockResolvedValue({ ok: true, data: { data: {items: [{id: '1'}]}, pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-nonarray'], mockAction, {}), {wrapper} ) await waitFor(() => { expect(result.current.loading).toBe(false) }) // Non-array data is returned as-is from the first page expect(result.current.data).toEqual({items: [{id: '1'}]}) }) it('returns null when data is null', async () => { const mockAction = vi.fn().mockResolvedValue({ ok: true, data: { data: null, pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-null-data'], mockAction, {}), {wrapper} ) await waitFor(() => { expect(result.current.loading).toBe(false) }) expect(result.current.data).toBeNull() }) }) describe('Pagination Logic', () => { it('appends data when fetching more pages', async () => { const page1 = [{id: '1'}] const page2 = [{id: '2'}] const mockAction = vi .fn() .mockResolvedValueOnce({ ok: true, data: { data: page1, pageInfo: {hasNextPage: true, endCursor: 'cursor1'}, }, }) .mockResolvedValueOnce({ ok: true, data: { data: page2, pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-pagination'], mockAction, {}), {wrapper} ) await waitFor(() => { expect(result.current.data).toEqual(page1) expect(result.current.hasNextPage).toBe(true) }) // Fetch more await act(async () => { await result.current.fetchMore() }) // Data should be flattened and appended await waitFor(() => { expect(result.current.data).toEqual([...page1, ...page2]) }) expect(result.current.hasNextPage).toBe(false) expect(mockAction).toHaveBeenCalledTimes(2) }) it('passes cursor to subsequent pages', async () => { const mockAction = vi .fn() .mockResolvedValueOnce({ ok: true, data: { data: [{id: '1'}], pageInfo: {hasNextPage: true, endCursor: 'cursor1'}, }, }) .mockResolvedValueOnce({ ok: true, data: { data: [{id: '2'}], pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-cursor'], mockAction, {}), {wrapper} ) await waitFor(() => { expect(result.current.data).toEqual([{id: '1'}]) expect(result.current.hasNextPage).toBe(true) }) await act(async () => { await result.current.fetchMore() }) // Second call should include the cursor expect(mockAction).toHaveBeenNthCalledWith(2, {after: 'cursor1'}) }) it('preserves other params when fetching more', async () => { const mockAction = vi .fn() .mockResolvedValueOnce({ ok: true, data: { data: [{id: '1'}], pageInfo: {hasNextPage: true, endCursor: 'cursor1'}, }, }) .mockResolvedValueOnce({ ok: true, data: { data: [{id: '2'}], pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-params'], mockAction, { query: 'shoes', fetchPolicy: 'cache-first', }), {wrapper} ) await waitFor(() => { expect(result.current.data).toEqual([{id: '1'}]) expect(result.current.hasNextPage).toBe(true) }) await act(async () => { await result.current.fetchMore() }) // Should preserve original params and add cursor expect(mockAction).toHaveBeenNthCalledWith(2, { query: 'shoes', fetchPolicy: 'cache-first', after: 'cursor1', }) }) }) describe('Error Handling', () => { it('handles action errors', async () => { const mockAction = vi.fn().mockRejectedValue(new Error('API Error')) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-api-error'], mockAction, {}), {wrapper} ) // Wait for loading to complete (after retries) await waitFor( () => { expect(result.current.loading).toBe(false) }, {timeout: 3000} ) expect(result.current.data).toBeNull() expect(result.current.error).toBeInstanceOf(Error) expect(result.current.error?.message).toBe('API Error') }) }) describe('Skip Parameter', () => { it('does not fetch when skip is true', async () => { const mockAction = vi.fn() renderHook( () => useShopActionInfiniteQuery( ['test-skip-inf'], mockAction, {}, {skip: true} ), {wrapper} ) await new Promise(resolve => setTimeout(resolve, 100)) expect(mockAction).not.toHaveBeenCalled() }) it('fetches when skip is false', async () => { const mockAction = vi.fn().mockResolvedValue({ ok: true, data: { data: [{id: 'noskip'}], pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery( ['test-noskip-fetch'], mockAction, {}, {skip: false} ), {wrapper} ) await waitFor(() => { expect(result.current.data).toEqual([{id: 'noskip'}]) }) expect(mockAction).toHaveBeenCalled() }) }) describe('API Contract', () => { it('returns expected shape', async () => { const mockAction = vi.fn().mockResolvedValue({ ok: true, data: { data: [{id: 'contract'}], pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-contract-inf'], mockAction, {}), {wrapper} ) await waitFor(() => { expect(result.current.loading).toBe(false) }) // Verify all expected properties exist expect(result.current).toHaveProperty('data') expect(result.current).toHaveProperty('loading') expect(result.current).toHaveProperty('error') expect(result.current).toHaveProperty('hasNextPage') expect(result.current).toHaveProperty('fetchMore') expect(result.current).toHaveProperty('refetch') // Verify types expect(typeof result.current.loading).toBe('boolean') expect(typeof result.current.hasNextPage).toBe('boolean') expect(typeof result.current.fetchMore).toBe('function') expect(typeof result.current.refetch).toBe('function') }) }) describe('hasNextPage Logic', () => { it('sets hasNextPage based on pageInfo', async () => { const mockAction = vi.fn().mockResolvedValue({ ok: true, data: { data: [{id: '1'}], pageInfo: {hasNextPage: true, endCursor: 'cursor1'}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-hasnext'], mockAction, {}), {wrapper} ) await waitFor(() => { expect(result.current.data).toEqual([{id: '1'}]) expect(result.current.hasNextPage).toBe(true) }) }) it('returns false when no more pages', async () => { const mockAction = vi.fn().mockResolvedValue({ ok: true, data: { data: [{id: '1'}], pageInfo: {hasNextPage: false, endCursor: null}, }, }) const {result} = renderHook( () => useShopActionInfiniteQuery(['test-nonext'], mockAction, {}), {wrapper} ) await waitFor(() => { expect(result.current.loading).toBe(false) }) expect(result.current.hasNextPage).toBe(false) }) }) })