/** * Tests for TanStack Query React Hooks * * Tests the hook factories and utility functions. * Note: These tests mock the TanStack Query hooks since we can't * actually use React hooks outside of a React component context. */ import { describe, it, expect, vi } from 'vitest' import { createUsePostgresQuery, createUsePostgresMutation, createUseLiveQuery, createUseCollectionInsert, createUseCollectionUpdate, createUseCollectionDelete, createUsePrefetchQuery, createUseInvalidateQueries, } from './hooks' import { createQueryAdapter } from './adapter' import type { BaseRecord, Collection } from './types' // ============================================================================ // Mock Setup // ============================================================================ const createMockFetch = () => vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ rows: [] }), }) const createMockAdapter = (fetch = createMockFetch()) => createQueryAdapter({ database: 'testdb', fetch, }) const createMockCollection = (items: T[] = []): Collection => { let _items = [...items] const subscribers = new Set<(items: T[]) => void>() return { id: 'test-collection', getAll: () => _items, get: (id) => _items.find(item => item.id === id), insert: vi.fn().mockImplementation(async (data) => { const newItem = { ...data, id: data.id ?? Date.now() } as T _items = [..._items, newItem] subscribers.forEach(cb => cb(_items)) return newItem }), update: vi.fn().mockImplementation(async (id, data) => { const existing = _items.find(item => item.id === id) if (!existing) throw new Error(`Item ${id} not found`) const updated = { ...existing, ...data } as T _items = _items.map(item => item.id === id ? updated : item) subscribers.forEach(cb => cb(_items)) return updated }), delete: vi.fn().mockImplementation(async (id) => { _items = _items.filter(item => item.id !== id) subscribers.forEach(cb => cb(_items)) }), subscribe: (callback: (items: T[]) => void) => { subscribers.add(callback) return () => subscribers.delete(callback) }, getSyncState: () => ({ connected: false, initialized: true, pendingCount: 0, }), } } // ============================================================================ // Hook Factory Tests // ============================================================================ describe('createUsePostgresQuery', () => { it('should create a hook function', () => { const mockUseQuery = vi.fn() const usePostgresQuery = createUsePostgresQuery(mockUseQuery) expect(typeof usePostgresQuery).toBe('function') }) it('should call useQuery with query options', () => { const mockUseQuery = vi.fn().mockReturnValue({ data: [], isLoading: false }) const usePostgresQuery = createUsePostgresQuery(mockUseQuery) const adapter = createMockAdapter() usePostgresQuery(adapter, 'SELECT * FROM users') expect(mockUseQuery).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['postgres', 'testdb', 'SELECT * FROM users'], queryFn: expect.any(Function), }) ) }) it('should accept params object', () => { const mockUseQuery = vi.fn().mockReturnValue({ data: [], isLoading: false }) const usePostgresQuery = createUsePostgresQuery(mockUseQuery) const adapter = createMockAdapter() usePostgresQuery(adapter, { sql: 'SELECT * FROM users WHERE id = $1', params: [123], staleTime: 30000, }) expect(mockUseQuery).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['postgres', 'testdb', 'SELECT * FROM users WHERE id = $1', 123], staleTime: 30000, }) ) }) it('should return useQuery result', () => { const mockResult = { data: [{ id: 1 }], isLoading: false, error: null } const mockUseQuery = vi.fn().mockReturnValue(mockResult) const usePostgresQuery = createUsePostgresQuery(mockUseQuery) const adapter = createMockAdapter() const result = usePostgresQuery(adapter, 'SELECT * FROM users') expect(result).toBe(mockResult) }) }) describe('createUsePostgresMutation', () => { it('should create a hook function', () => { const mockUseMutation = vi.fn() const mockUseQueryClient = vi.fn().mockReturnValue({}) const usePostgresMutation = createUsePostgresMutation(mockUseMutation, mockUseQueryClient) expect(typeof usePostgresMutation).toBe('function') }) it('should call useMutation with mutation options', () => { const mockUseMutation = vi.fn().mockReturnValue({ mutate: vi.fn() }) const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: vi.fn(), }) const usePostgresMutation = createUsePostgresMutation(mockUseMutation, mockUseQueryClient) const adapter = createMockAdapter() usePostgresMutation(adapter) expect(mockUseMutation).toHaveBeenCalledWith( expect.objectContaining({ mutationKey: ['postgres', 'testdb', 'mutation'], mutationFn: expect.any(Function), }) ) }) it('should invalidate all queries when invalidate.all is true', async () => { const mockInvalidateQueries = vi.fn() const mockUseMutation = vi.fn().mockImplementation((options) => { // Simulate calling onSuccess return { mutate: vi.fn((variables) => { options.onSuccess?.({ rows: [] }, variables, undefined) }), } }) const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: mockInvalidateQueries, }) const usePostgresMutation = createUsePostgresMutation(mockUseMutation, mockUseQueryClient) const adapter = createMockAdapter() const { mutate } = usePostgresMutation(adapter, { invalidate: { all: true }, }) as { mutate: (v: { sql: string }) => void } mutate({ sql: 'INSERT INTO users (name) VALUES ($1)' }) expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['postgres', 'testdb'], }) }) it('should invalidate table queries when invalidate.tables is provided', async () => { const mockInvalidateQueries = vi.fn() const mockUseMutation = vi.fn().mockImplementation((options) => { return { mutate: vi.fn((variables) => { options.onSuccess?.({ rows: [] }, variables, undefined) }), } }) const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: mockInvalidateQueries, }) const usePostgresMutation = createUsePostgresMutation(mockUseMutation, mockUseQueryClient) const adapter = createMockAdapter() const { mutate } = usePostgresMutation(adapter, { invalidate: { tables: ['users'] }, }) as { mutate: (v: { sql: string }) => void } mutate({ sql: 'INSERT INTO users (name) VALUES ($1)' }) expect(mockInvalidateQueries).toHaveBeenCalled() }) it('should call user-provided onSuccess callback', async () => { const userOnSuccess = vi.fn() const mockUseMutation = vi.fn().mockImplementation((options) => { return { mutate: vi.fn((variables) => { options.onSuccess?.({ rows: [] }, variables, undefined) }), } }) const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: vi.fn(), }) const usePostgresMutation = createUsePostgresMutation(mockUseMutation, mockUseQueryClient) const adapter = createMockAdapter() const { mutate } = usePostgresMutation(adapter, { onSuccess: userOnSuccess, }) as { mutate: (v: { sql: string }) => void } mutate({ sql: 'INSERT INTO users (name) VALUES ($1)' }) expect(userOnSuccess).toHaveBeenCalledWith( { rows: [] }, { sql: 'INSERT INTO users (name) VALUES ($1)' }, undefined ) }) }) describe('createUseLiveQuery', () => { it('should create a hook function', () => { const mockUseSyncExternalStore = vi.fn() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) expect(typeof useLiveQuery).toBe('function') }) it('should call useSyncExternalStore with subscribe and getSnapshot', () => { const mockUseSyncExternalStore = vi.fn().mockReturnValue([]) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([{ id: 1, name: 'Alice' }]) useLiveQuery(collection) expect(mockUseSyncExternalStore).toHaveBeenCalledWith( expect.any(Function), // subscribe expect.any(Function), // getSnapshot expect.any(Function), // getServerSnapshot ) }) it('should return all items by default', () => { const items = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection) expect(result).toEqual(items) }) it('should filter by where object', () => { const items = [ { id: 1, name: 'Alice', active: true }, { id: 2, name: 'Bob', active: false }, { id: 3, name: 'Charlie', active: true }, ] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection, { where: { active: true } }) expect(result).toEqual([ { id: 1, name: 'Alice', active: true }, { id: 3, name: 'Charlie', active: true }, ]) }) it('should filter by where function', () => { const items = [ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 35 }, { id: 3, name: 'Charlie', age: 20 }, ] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection, { where: (item) => item.age >= 25, }) expect(result).toEqual([ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 35 }, ]) }) it('should sort by orderBy ascending', () => { const items = [ { id: 2, name: 'Bob' }, { id: 1, name: 'Alice' }, { id: 3, name: 'Charlie' }, ] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection, { orderBy: { field: 'name', direction: 'asc' }, }) expect(result.map(i => i.name)).toEqual(['Alice', 'Bob', 'Charlie']) }) it('should sort by orderBy descending', () => { const items = [ { id: 2, name: 'Bob' }, { id: 1, name: 'Alice' }, { id: 3, name: 'Charlie' }, ] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection, { orderBy: { field: 'name', direction: 'desc' }, }) expect(result.map(i => i.name)).toEqual(['Charlie', 'Bob', 'Alice']) }) it('should apply limit', () => { const items = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, ] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection, { limit: 2 }) expect(result).toHaveLength(2) }) it('should apply offset', () => { const items = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, ] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection, { offset: 1 }) expect(result).toEqual([ { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, ]) }) it('should apply offset and limit together', () => { const items = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, { id: 4, name: 'David' }, ] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection, { offset: 1, limit: 2 }) expect(result).toEqual([ { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, ]) }) it('should return empty array when disabled', () => { const items = [{ id: 1, name: 'Alice' }] const mockUseSyncExternalStore = vi.fn().mockImplementation((_subscribe, getSnapshot) => { return getSnapshot() }) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection(items) const result = useLiveQuery(collection, { enabled: false }) expect(result).toEqual([]) }) it('should set up subscription correctly', () => { const mockUseSyncExternalStore = vi.fn().mockReturnValue([]) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([]) useLiveQuery(collection) const subscribeArg = mockUseSyncExternalStore.mock.calls[0]![0] const mockCallback = vi.fn() const unsubscribe = subscribeArg(mockCallback) expect(typeof unsubscribe).toBe('function') }) }) // ============================================================================ // Collection Hook Factory Tests // ============================================================================ describe('createUseCollectionInsert', () => { it('should create a hook function', () => { const mockUseMutation = vi.fn() const useCollectionInsert = createUseCollectionInsert(mockUseMutation) expect(typeof useCollectionInsert).toBe('function') }) it('should call useMutation with correct options', () => { const mockUseMutation = vi.fn().mockReturnValue({ mutate: vi.fn() }) const useCollectionInsert = createUseCollectionInsert(mockUseMutation) const collection = createMockCollection<{ id: number; name: string }>([]) useCollectionInsert(collection) expect(mockUseMutation).toHaveBeenCalledWith( expect.objectContaining({ mutationKey: ['postgres', 'collection', 'test-collection', 'insert'], mutationFn: expect.any(Function), }) ) }) it('mutationFn should call collection.insert', async () => { let capturedMutationFn: ((data: { name: string }) => Promise) | undefined const mockUseMutation = vi.fn().mockImplementation((options) => { capturedMutationFn = options.mutationFn return { mutate: vi.fn() } }) const useCollectionInsert = createUseCollectionInsert(mockUseMutation) const collection = createMockCollection<{ id: number; name: string }>([]) useCollectionInsert(collection) expect(capturedMutationFn).toBeDefined() await capturedMutationFn!({ name: 'Alice' }) expect(collection.insert).toHaveBeenCalledWith({ name: 'Alice' }) }) }) describe('createUseCollectionUpdate', () => { it('should create a hook function', () => { const mockUseMutation = vi.fn() const useCollectionUpdate = createUseCollectionUpdate(mockUseMutation) expect(typeof useCollectionUpdate).toBe('function') }) it('should call useMutation with correct options', () => { const mockUseMutation = vi.fn().mockReturnValue({ mutate: vi.fn() }) const useCollectionUpdate = createUseCollectionUpdate(mockUseMutation) const collection = createMockCollection<{ id: number; name: string }>([]) useCollectionUpdate(collection) expect(mockUseMutation).toHaveBeenCalledWith( expect.objectContaining({ mutationKey: ['postgres', 'collection', 'test-collection', 'update'], mutationFn: expect.any(Function), }) ) }) it('mutationFn should call collection.update', async () => { let capturedMutationFn: ((args: { id: number; data: { name: string } }) => Promise) | undefined const mockUseMutation = vi.fn().mockImplementation((options) => { capturedMutationFn = options.mutationFn return { mutate: vi.fn() } }) const useCollectionUpdate = createUseCollectionUpdate(mockUseMutation) const collection = createMockCollection<{ id: number; name: string }>([{ id: 1, name: 'Alice' }]) useCollectionUpdate(collection) expect(capturedMutationFn).toBeDefined() await capturedMutationFn!({ id: 1, data: { name: 'Alice Updated' } }) expect(collection.update).toHaveBeenCalledWith(1, { name: 'Alice Updated' }) }) }) describe('createUseCollectionDelete', () => { it('should create a hook function', () => { const mockUseMutation = vi.fn() const useCollectionDelete = createUseCollectionDelete(mockUseMutation) expect(typeof useCollectionDelete).toBe('function') }) it('should call useMutation with correct options', () => { const mockUseMutation = vi.fn().mockReturnValue({ mutate: vi.fn() }) const useCollectionDelete = createUseCollectionDelete(mockUseMutation) const collection = createMockCollection<{ id: number; name: string }>([]) useCollectionDelete(collection) expect(mockUseMutation).toHaveBeenCalledWith( expect.objectContaining({ mutationKey: ['postgres', 'collection', 'test-collection', 'delete'], mutationFn: expect.any(Function), }) ) }) it('mutationFn should call collection.delete', async () => { let capturedMutationFn: ((id: number) => Promise) | undefined const mockUseMutation = vi.fn().mockImplementation((options) => { capturedMutationFn = options.mutationFn return { mutate: vi.fn() } }) const useCollectionDelete = createUseCollectionDelete(mockUseMutation) const collection = createMockCollection<{ id: number; name: string }>([{ id: 1, name: 'Alice' }]) useCollectionDelete(collection) expect(capturedMutationFn).toBeDefined() await capturedMutationFn!(1) expect(collection.delete).toHaveBeenCalledWith(1) }) }) // ============================================================================ // Utility Hook Factory Tests // ============================================================================ describe('createUsePrefetchQuery', () => { it('should create a hook function', () => { const mockUseQueryClient = vi.fn() const usePrefetchQuery = createUsePrefetchQuery(mockUseQueryClient) expect(typeof usePrefetchQuery).toBe('function') }) it('should return a prefetch function', () => { const mockPrefetchQuery = vi.fn() const mockUseQueryClient = vi.fn().mockReturnValue({ prefetchQuery: mockPrefetchQuery, }) const usePrefetchQuery = createUsePrefetchQuery(mockUseQueryClient) const adapter = createMockAdapter() const prefetch = usePrefetchQuery(adapter) expect(typeof prefetch).toBe('function') }) it('prefetch function should call queryClient.prefetchQuery', async () => { const mockPrefetchQuery = vi.fn().mockResolvedValue(undefined) const mockUseQueryClient = vi.fn().mockReturnValue({ prefetchQuery: mockPrefetchQuery, }) const usePrefetchQuery = createUsePrefetchQuery(mockUseQueryClient) const adapter = createMockAdapter() const prefetch = usePrefetchQuery(adapter) await prefetch('SELECT * FROM users') expect(mockPrefetchQuery).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['postgres', 'testdb', 'SELECT * FROM users'], }) ) }) }) describe('createUseInvalidateQueries', () => { it('should create a hook function', () => { const mockUseQueryClient = vi.fn() const useInvalidateQueries = createUseInvalidateQueries(mockUseQueryClient) expect(typeof useInvalidateQueries).toBe('function') }) it('should return an object with invalidation methods', () => { const mockInvalidateQueries = vi.fn() const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: mockInvalidateQueries, }) const useInvalidateQueries = createUseInvalidateQueries(mockUseQueryClient) const adapter = createMockAdapter() const result = useInvalidateQueries(adapter) expect(result).toHaveProperty('invalidateQuery') expect(result).toHaveProperty('invalidateTables') expect(result).toHaveProperty('invalidateAll') }) it('invalidateQuery should invalidate specific query', () => { const mockInvalidateQueries = vi.fn() const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: mockInvalidateQueries, }) const useInvalidateQueries = createUseInvalidateQueries(mockUseQueryClient) const adapter = createMockAdapter() const { invalidateQuery } = useInvalidateQueries(adapter) invalidateQuery('SELECT * FROM users') expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['postgres', 'testdb', 'SELECT * FROM users'], }) }) it('invalidateQuery should handle params', () => { const mockInvalidateQueries = vi.fn() const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: mockInvalidateQueries, }) const useInvalidateQueries = createUseInvalidateQueries(mockUseQueryClient) const adapter = createMockAdapter() const { invalidateQuery } = useInvalidateQueries(adapter) invalidateQuery({ sql: 'SELECT * FROM users WHERE id = $1', params: [123] }) expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['postgres', 'testdb', 'SELECT * FROM users WHERE id = $1', 123], }) }) it('invalidateTables should invalidate database queries', () => { const mockInvalidateQueries = vi.fn() const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: mockInvalidateQueries, }) const useInvalidateQueries = createUseInvalidateQueries(mockUseQueryClient) const adapter = createMockAdapter() const { invalidateTables } = useInvalidateQueries(adapter) invalidateTables(['users', 'orders']) expect(mockInvalidateQueries).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['postgres', 'testdb'], }) ) }) it('invalidateAll should invalidate all database queries', () => { const mockInvalidateQueries = vi.fn() const mockUseQueryClient = vi.fn().mockReturnValue({ invalidateQueries: mockInvalidateQueries, }) const useInvalidateQueries = createUseInvalidateQueries(mockUseQueryClient) const adapter = createMockAdapter() const { invalidateAll } = useInvalidateQueries(adapter) invalidateAll() expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['postgres', 'testdb'], }) }) })