diff --git a/examples/useCreateMutation-example.tsx b/examples/useCreateMutation-example.tsx index a9523c1..142bcbe 100644 --- a/examples/useCreateMutation-example.tsx +++ b/examples/useCreateMutation-example.tsx @@ -13,37 +13,38 @@ function App() { } function CreateMutationExample() { - const { mutate: createPost, isPending, isSuccess, error } = useCreateMutation('posts'); const { data: posts } = useCollection('posts', { perPage: 10 }); + const { mutateAsync: createPost, isPending, isSuccess, isError, error } = useCreateMutation('posts'); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [status, setStatus] = useState<'draft' | 'published'>('draft'); const handleCreate = async (e: FormEvent) => { e.preventDefault(); + if (!title.trim() || !content.trim()) return; + try { const newPost = await createPost({ - title, - content, + title: title.trim(), + content: content.trim(), status, }); console.log('Post created:', newPost); setTitle(''); setContent(''); + setStatus('draft'); } catch (err) { console.error('Failed to create post:', err); } }; - if (error) return
Error: {error}
; - return (
-

Create Post

+

Create New Post

-

Create New Post

+

Create Post

setTitle(e.target.value)} placeholder="Post title" required />
@@ -60,6 +61,7 @@ function CreateMutationExample() { {isPending ? 'Creating...' : 'Create Post'} {isSuccess &&

Post created successfully!

} + {isError && error &&

Error: {error}

}
diff --git a/examples/useDeleteMutation-example.tsx b/examples/useDeleteMutation-example.tsx index 561c708..d879bfe 100644 --- a/examples/useDeleteMutation-example.tsx +++ b/examples/useDeleteMutation-example.tsx @@ -13,28 +13,34 @@ function App() { } function DeleteMutationExample() { - const { mutate: deletePost, isPending, isSuccess, error } = useDeleteMutation('posts'); const { data: posts } = useCollection('posts', { perPage: 10 }); const [deletingId, setDeletingId] = useState(null); + const { mutateAsync: deletePost, isPending, isSuccess, isError, error } = useDeleteMutation('posts', deletingId); - const handleDelete = async (id: string) => { - if (window.confirm('Are you sure you want to delete this post?')) { - setDeletingId(id); - try { - const success = await deletePost(id); - if (success) { - console.log('Post deleted successfully'); - } - } catch (err) { - console.error('Failed to delete post:', err); - } finally { - setDeletingId(null); - } + const handleDelete = async (postId: string) => { + if (isPending) { + return; + } + + setDeletingId(postId); + try { + await deletePost(); + console.log('Post deleted successfully'); + } catch (err) { + console.error('Failed to delete post:', err); + } finally { + setDeletingId(null); + } + }; + + const confirmDelete = (post: RecordModel) => { + if (window.confirm(`Are you sure you want to delete "${post.title}"?`)) { + handleDelete(post.id); } }; - if (error) return
Error: {error}
; + if (isError) return
Error: {error}
; return (
@@ -53,18 +59,17 @@ function DeleteMutationExample() {
diff --git a/examples/useUpdateMutation-example.tsx b/examples/useUpdateMutation-example.tsx index a655f5b..267b599 100644 --- a/examples/useUpdateMutation-example.tsx +++ b/examples/useUpdateMutation-example.tsx @@ -13,10 +13,10 @@ function App() { } function UpdateMutationExample() { - const { mutate: updatePost, isPending, isSuccess, error } = useUpdateMutation('posts'); const { data: posts } = useCollection('posts', { perPage: 10 }); const [editingId, setEditingId] = useState(null); + const { mutateAsync: updatePost, isPending, isSuccess, isError, error } = useUpdateMutation('posts', editingId); const [editTitle, setEditTitle] = useState(''); const [editContent, setEditContent] = useState(''); const [editStatus, setEditStatus] = useState<'draft' | 'published'>('draft'); @@ -26,7 +26,7 @@ function UpdateMutationExample() { if (!editingId) return; try { - const updatedPost = await updatePost(editingId, { + const updatedPost = await updatePost({ title: editTitle, content: editContent, status: editStatus, @@ -53,8 +53,6 @@ function UpdateMutationExample() { setEditContent(''); }; - if (error) return
Error: {error}
; - return (

Update Posts

@@ -81,6 +79,7 @@ function UpdateMutationExample() { Cancel {isSuccess &&

Post updated successfully!

} + {isError && error &&

Error: {error}

} )} diff --git a/src/hooks/useCreateMutation.ts b/src/hooks/useCreateMutation.ts index 0b40285..66f6bc9 100644 --- a/src/hooks/useCreateMutation.ts +++ b/src/hooks/useCreateMutation.ts @@ -12,12 +12,14 @@ import { usePocketBase } from './usePocketBase'; * * @example * ```tsx - * const { mutate, isPending, isSuccess, error } = useCreateMutation('posts'); + * const { mutateAsync, isPending, isSuccess, isError, error } = useCreateMutation('posts'); * * const handleCreate = async () => { - * const newPost = await mutate({ title: 'Hello', content: 'World' }); - * if (newPost) { + * try { + * const newPost = await mutateAsync({ title: 'Hello', content: 'World' }); * console.log('Created:', newPost); + * } catch (err) { + * console.error('Failed to create post:', err); * } * }; * ``` @@ -28,16 +30,17 @@ export function useCreateMutation(collectionName: st const [isPending, setIsPending] = useState(false); const [error, setError] = useState(null); - const mutate = useCallback( - async (bodyParams: Partial, options?: RecordOptions): Promise => { + const mutateAsync = useCallback( + async (bodyParams: Partial, options?: RecordOptions): Promise => { try { setIsPending(true); setError(null); - const record = await recordService.create(bodyParams, options); + const record = options ? await recordService.create(bodyParams, options) : await recordService.create(bodyParams); return record as Record; } catch (err) { - setError(err instanceof Error ? err.message : 'Error creating record'); - return null; + const errorMessage = err instanceof Error ? err.message : 'Error creating record'; + setError(errorMessage); + throw new Error(errorMessage); } finally { setIsPending(false); } @@ -45,13 +48,24 @@ export function useCreateMutation(collectionName: st [recordService], ); + const mutate = useCallback( + (bodyParams: Partial, options?: RecordOptions): void => { + mutateAsync(bodyParams, options).catch(() => { + // Error is already handled in mutateAsync + }); + }, + [mutateAsync], + ); + return useMemo( (): UseCreateMutationResult => ({ mutate, + mutateAsync, isPending, + isError: !!error, error, isSuccess: !isPending && !error, }), - [mutate, isPending, error], + [mutate, mutateAsync, isPending, error], ); } diff --git a/src/hooks/useDeleteMutation.ts b/src/hooks/useDeleteMutation.ts index 4279faa..8397cf1 100644 --- a/src/hooks/useDeleteMutation.ts +++ b/src/hooks/useDeleteMutation.ts @@ -1,4 +1,4 @@ -import type { CommonOptions } from 'pocketbase'; +import type { CommonOptions, RecordModel } from 'pocketbase'; import { useCallback, useMemo, useState } from 'react'; import type { UseDeleteMutationResult } from '../types'; import { usePocketBase } from './usePocketBase'; @@ -7,6 +7,7 @@ import { usePocketBase } from './usePocketBase'; * Hook for deleting records from a PocketBase collection. * * @param collectionName - The name of the PocketBase collection + * @param id - The ID of the record to delete * @returns An object containing the mutate function and mutation state * * @example @@ -21,36 +22,51 @@ import { usePocketBase } from './usePocketBase'; * }; * ``` */ -export function useDeleteMutation(collectionName: string): UseDeleteMutationResult { +export function useDeleteMutation(collectionName: string, id: Record['id'] | null): UseDeleteMutationResult { const pb = usePocketBase(); const recordService = useMemo(() => pb.collection(collectionName), [pb, collectionName]); const [isPending, setIsPending] = useState(false); const [error, setError] = useState(null); - const mutate = useCallback( - async (id: string, options?: CommonOptions): Promise => { + const mutateAsync = useCallback( + async (options?: CommonOptions): Promise => { + if (!id) { + throw new Error('ID is required'); + } + try { setIsPending(true); setError(null); await recordService.delete(id, options); - return true; } catch (err) { - setError(err instanceof Error ? err.message : 'Error deleting record'); - return false; + const errorMessage = err instanceof Error ? err.message : 'Error deleting record'; + setError(errorMessage); + throw new Error(errorMessage); } finally { setIsPending(false); } }, - [recordService], + [recordService, id], + ); + + const mutate = useCallback( + (options?: CommonOptions): void => { + mutateAsync(options).catch(() => { + // Error is already handled in mutateAsync + }); + }, + [mutateAsync], ); return useMemo( (): UseDeleteMutationResult => ({ mutate, + mutateAsync, isPending, + isError: !!error, error, isSuccess: !isPending && !error, }), - [mutate, isPending, error], + [mutate, mutateAsync, isPending, error], ); } diff --git a/src/hooks/useUpdateMutation.ts b/src/hooks/useUpdateMutation.ts index 55f32fd..a179eaa 100644 --- a/src/hooks/useUpdateMutation.ts +++ b/src/hooks/useUpdateMutation.ts @@ -8,6 +8,7 @@ import { usePocketBase } from './usePocketBase'; * * @template Record - The record type extending RecordModel * @param collectionName - The name of the PocketBase collection + * @param id - The ID of the record to update * @returns An object containing the mutate function and mutation state * * @example @@ -22,36 +23,52 @@ import { usePocketBase } from './usePocketBase'; * }; * ``` */ -export function useUpdateMutation(collectionName: string): UseUpdateMutationResult { +export function useUpdateMutation(collectionName: string, id: Record['id'] | null): UseUpdateMutationResult { const pb = usePocketBase(); const recordService = useMemo(() => pb.collection(collectionName), [pb, collectionName]); const [isPending, setIsPending] = useState(false); const [error, setError] = useState(null); - const mutate = useCallback( - async (id: string, bodyParams: Partial, options?: RecordOptions): Promise => { + const mutateAsync = useCallback( + async (bodyParams: Partial, options?: RecordOptions): Promise => { + if (!id) { + throw new Error('ID is required'); + } + try { setIsPending(true); setError(null); const record = options ? await recordService.update(id, bodyParams, options) : await recordService.update(id, bodyParams); return record as Record; } catch (err) { - setError(err instanceof Error ? err.message : 'Error updating record'); - return null; + const errorMessage = err instanceof Error ? err.message : 'Error updating record'; + setError(errorMessage); + throw new Error(errorMessage); } finally { setIsPending(false); } }, - [recordService], + [recordService, id], + ); + + const mutate = useCallback( + (bodyParams: Partial, options?: RecordOptions): void => { + mutateAsync(bodyParams, options).catch(() => { + // Error is already handled in mutateAsync + }); + }, + [mutateAsync], ); return useMemo( (): UseUpdateMutationResult => ({ mutate, + mutateAsync, isPending, + isError: !!error, error, isSuccess: !isPending && !error, }), - [mutate, isPending, error], + [mutate, mutateAsync, isPending, error], ); } diff --git a/src/types/index.ts b/src/types/index.ts index b25ee23..86aa6c2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,8 +3,8 @@ export * from './query-result.type'; export * from './record-transformer.type'; export * from './useAuth.type'; export * from './useCollection.type'; -export * from './useCommon.type'; export * from './useCreateMutation.type'; export * from './useDeleteMutation.type'; +export * from './useQueryCommon.type'; export * from './useRecord.type'; export * from './useUpdateMutation.type'; diff --git a/src/types/useCollection.type.ts b/src/types/useCollection.type.ts index bce43c4..964b6bf 100644 --- a/src/types/useCollection.type.ts +++ b/src/types/useCollection.type.ts @@ -1,13 +1,13 @@ import type { RecordModel } from 'pocketbase'; import type { QueryResult } from './query-result.type'; -import type { UseCommonOptions } from './useCommon.type'; +import type { UseQueryCommonOptions } from './useQueryCommon.type'; /** * Options for configuring the useCollection hook. * * @template T - The record type extending RecordModel */ -export interface UseCollectionOptions extends UseCommonOptions { +export interface UseCollectionOptions extends UseQueryCommonOptions { /** * PocketBase filter query (e.g., 'published = true') */ diff --git a/src/types/useCreateMutation.type.ts b/src/types/useCreateMutation.type.ts index e9542af..3e6a8fc 100644 --- a/src/types/useCreateMutation.type.ts +++ b/src/types/useCreateMutation.type.ts @@ -1,31 +1,25 @@ import type { RecordModel, RecordOptions } from 'pocketbase'; +import type { UseMutationCommonOptions } from './useMutationCommon.type'; /** * Result type returned by useCreateMutation hook. * * @template Record - The record type extending RecordModel */ -export interface UseCreateMutationResult { +export interface UseCreateMutationResult extends UseMutationCommonOptions { /** - * Function to create a new record. Returns the created record on success, null on error. + * Function to create a new record synchronously. Triggers the mutation but doesn't wait for completion. * * @param bodyParams - Partial record data to create * @param options - Optional PocketBase record options (expand, fields, etc.) */ - mutate: (bodyParams: Partial, options?: RecordOptions) => Promise; + mutate: (bodyParams: Partial, options?: RecordOptions) => void; /** - * True when the mutation is in progress - */ - isPending: boolean; - - /** - * True when the mutation completed successfully (not pending and no error) - */ - isSuccess: boolean; - - /** - * Error message if the mutation failed, null otherwise + * Function to create a new record asynchronously. Returns a promise that resolves with the created record. + * + * @param bodyParams - Partial record data to create + * @param options - Optional PocketBase record options (expand, fields, etc.) */ - error: string | null; + mutateAsync: (bodyParams: Partial, options?: RecordOptions) => Promise; } diff --git a/src/types/useDeleteMutation.type.ts b/src/types/useDeleteMutation.type.ts index b10b7d1..a8f14eb 100644 --- a/src/types/useDeleteMutation.type.ts +++ b/src/types/useDeleteMutation.type.ts @@ -1,29 +1,21 @@ import type { CommonOptions } from 'pocketbase'; +import type { UseMutationCommonOptions } from './useMutationCommon.type'; /** * Result type returned by useDeleteMutation hook. */ -export interface UseDeleteMutationResult { +export interface UseDeleteMutationResult extends UseMutationCommonOptions { /** - * Function to delete a record. Returns true on success, false on error. + * Function to delete a record synchronously. Triggers the mutation but doesn't wait for completion. * - * @param id - The ID of the record to delete * @param options - Optional PocketBase common options (headers, fetch, etc.) */ - mutate: (id: string, options?: CommonOptions) => Promise; + mutate: (options?: CommonOptions) => void; /** - * True when the mutation is in progress - */ - isPending: boolean; - - /** - * True when the mutation completed successfully (not pending and no error) - */ - isSuccess: boolean; - - /** - * Error message if the mutation failed, null otherwise + * Function to delete a record asynchronously. Returns a promise that resolves when deletion is complete. + * + * @param options - Optional PocketBase common options (headers, fetch, etc.) */ - error: string | null; + mutateAsync: (options?: CommonOptions) => Promise; } diff --git a/src/types/useMutationCommon.type.ts b/src/types/useMutationCommon.type.ts new file mode 100644 index 0000000..002b301 --- /dev/null +++ b/src/types/useMutationCommon.type.ts @@ -0,0 +1,21 @@ +export interface UseMutationCommonOptions { + /** + * True when the mutation is in progress + */ + isPending: boolean; + + /** + * True when the mutation completed successfully (not pending and no error) + */ + isSuccess: boolean; + + /** + * True when the mutation failed + */ + isError: boolean; + + /** + * Error message if the mutation failed, null otherwise + */ + error: string | null; +} diff --git a/src/types/useCommon.type.ts b/src/types/useQueryCommon.type.ts similarity index 85% rename from src/types/useCommon.type.ts rename to src/types/useQueryCommon.type.ts index 98ef26e..e70e554 100644 --- a/src/types/useCommon.type.ts +++ b/src/types/useQueryCommon.type.ts @@ -1,7 +1,7 @@ import type { RecordModel } from 'pocketbase'; import type { RecordTransformer } from './record-transformer.type'; -export interface UseCommonOptions { +export interface UseQueryCommonOptions { /** * Expand related records (e.g., 'author,comments') */ diff --git a/src/types/useRecord.type.ts b/src/types/useRecord.type.ts index b116894..d7fff93 100644 --- a/src/types/useRecord.type.ts +++ b/src/types/useRecord.type.ts @@ -1,13 +1,13 @@ import type { RecordModel } from 'pocketbase'; import type { QueryResult } from './query-result.type'; -import type { UseCommonOptions } from './useCommon.type'; +import type { UseQueryCommonOptions } from './useQueryCommon.type'; /** * Options for configuring the useRecord hook. * * @template T - The record type extending RecordModel */ -export interface UseRecordOptions extends UseCommonOptions { +export interface UseRecordOptions extends UseQueryCommonOptions { /** * Default value to use before data is loaded */ diff --git a/src/types/useUpdateMutation.type.ts b/src/types/useUpdateMutation.type.ts index 335714a..6d26e0e 100644 --- a/src/types/useUpdateMutation.type.ts +++ b/src/types/useUpdateMutation.type.ts @@ -1,32 +1,25 @@ import type { RecordModel, RecordOptions } from 'pocketbase'; +import type { UseMutationCommonOptions } from './useMutationCommon.type'; /** * Result type returned by useUpdateMutation hook. * * @template Record - The record type extending RecordModel */ -export interface UseUpdateMutationResult { +export interface UseUpdateMutationResult extends UseMutationCommonOptions { /** - * Function to update an existing record. Returns the updated record on success, null on error. + * Function to update an existing record synchronously. Triggers the mutation but doesn't wait for completion. * - * @param id - The ID of the record to update * @param bodyParams - Partial record data to update * @param options - Optional PocketBase record options (expand, fields, etc.) */ - mutate: (id: string, bodyParams: Partial, options?: RecordOptions) => Promise; + mutate: (bodyParams: Partial, options?: RecordOptions) => void; /** - * True when the mutation is in progress - */ - isPending: boolean; - - /** - * True when the mutation completed successfully (not pending and no error) - */ - isSuccess: boolean; - - /** - * Error message if the mutation failed, null otherwise + * Function to update an existing record asynchronously. Returns a promise that resolves with the updated record. + * + * @param bodyParams - Partial record data to update + * @param options - Optional PocketBase record options (expand, fields, etc.) */ - error: string | null; + mutateAsync: (bodyParams: Partial, options?: RecordOptions) => Promise; } diff --git a/tests/hooks/useCollection.test.tsx b/tests/hooks/useCollection.test.tsx index 9be06b0..381563c 100644 --- a/tests/hooks/useCollection.test.tsx +++ b/tests/hooks/useCollection.test.tsx @@ -738,7 +738,7 @@ describe('useCollection', () => { return Promise.resolve(() => {}); }); - const faultyTransformer = (record: any) => { + const faultyTransformer = (_record: any) => { throw new Error('Transformer error'); }; diff --git a/tests/hooks/useCreateMutation.test.tsx b/tests/hooks/useCreateMutation.test.tsx index 9118088..97e0a58 100644 --- a/tests/hooks/useCreateMutation.test.tsx +++ b/tests/hooks/useCreateMutation.test.tsx @@ -22,113 +22,183 @@ describe('useCreateMutation', () => { expect(result.current.isPending).toBe(false); expect(result.current.error).toBe(null); expect(result.current.isSuccess).toBe(true); + expect(result.current.isError).toBe(false); expect(typeof result.current.mutate).toBe('function'); + expect(typeof result.current.mutateAsync).toBe('function'); }); - it('should handle create mutation', async () => { - const mockData = { id: '1', title: 'New Item' }; - mockCreate.mockResolvedValue(mockData); + it('should throw error when used outside provider', () => { + expect(() => { + renderHook(() => useCreateMutation('test')); + }).toThrow('usePocketBase must be used within a PocketBaseProvider'); + }); - const wrapper = createWrapper(mockPocketBase); + describe('mutateAsync', () => { + it('should handle successful create', async () => { + const mockData = { id: '1', title: 'New Item' }; + mockCreate.mockResolvedValue(mockData); - const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + const wrapper = createWrapper(mockPocketBase); - const mutationResult = await result.current.mutate({ - title: 'New Item', - }); + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); - return waitFor(() => { - expect(mockCreate).toHaveBeenCalledWith( - { - title: 'New Item', - }, - undefined, - ); + const mutationResult = await result.current.mutateAsync({ + title: 'New Item', + }); + + expect(mockCreate).toHaveBeenCalledWith({ title: 'New Item' }); expect(mutationResult).toEqual(mockData); expect(result.current.isPending).toBe(false); expect(result.current.isSuccess).toBe(true); }); - }); - it('should handle create mutation with options', async () => { - const mockData = { id: '1', title: 'New Item' }; - mockCreate.mockResolvedValue(mockData); + it('should handle create with options', async () => { + const mockData = { id: '1', title: 'New Item' }; + mockCreate.mockResolvedValue(mockData); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); - const options = { expand: 'relation' }; + const options = { expand: 'relation' }; + await result.current.mutateAsync({ title: 'New Item' }, options); - await result.current.mutate({ title: 'New Item' }, options); - - return waitFor(() => { expect(mockCreate).toHaveBeenCalledWith({ title: 'New Item' }, options); }); - }); - it('should handle mutation error', async () => { - const mockError = new Error('Create failed'); - mockCreate.mockRejectedValue(mockError); + it('should handle mutation error', async () => { + const mockError = new Error('Create failed'); + mockCreate.mockRejectedValue(mockError); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); - await result.current.mutate({ title: 'Test' }); + await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('Create failed'); - return waitFor(() => { - expect(result.current.error).toEqual('Create failed'); - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(false); + await waitFor(() => { + expect(result.current.error).toEqual('Create failed'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); }); - }); - it('should handle non-Error exceptions', async () => { - mockCreate.mockRejectedValue('String error'); + it('should handle non-Error exceptions', async () => { + mockCreate.mockRejectedValue('String error'); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); - await result.current.mutate({ title: 'Test' }); + await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('Error creating record'); - return waitFor(() => { - expect(result.current.error).toBe('Error creating record'); - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(false); + await waitFor(() => { + expect(result.current.error).toBe('Error creating record'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); + }); + + it('should set isPending to true during mutation', async () => { + let resolveCreate: (value: unknown) => void = () => {}; + const createPromise = new Promise((resolve) => { + resolveCreate = resolve; + }); + mockCreate.mockReturnValue(createPromise); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + + const mutationPromise = result.current.mutateAsync({ title: 'Test' }); + + await waitFor(() => { + expect(result.current.isPending).toBe(true); + expect(result.current.isSuccess).toBe(false); + }); + + resolveCreate({ id: '1', title: 'Test' }); + await mutationPromise; + + await waitFor(() => { + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); }); - it('should set isPending to true during mutation', async () => { - let resolveCreate: (value: unknown) => void; - const createPromise = new Promise((resolve) => { - resolveCreate = resolve; + describe('mutate', () => { + it('should handle successful create', async () => { + const mockData = { id: '1', title: 'New Item' }; + mockCreate.mockResolvedValue(mockData); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + + const mutateResult = result.current.mutate({ title: 'New Item' }); + expect(mutateResult).toBeUndefined(); + + await waitFor(() => { + expect(mockCreate).toHaveBeenCalledWith({ title: 'New Item' }); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); - mockCreate.mockReturnValue(createPromise); - const wrapper = createWrapper(mockPocketBase); + it('should handle create with options', async () => { + const mockData = { id: '1', title: 'New Item' }; + mockCreate.mockResolvedValue(mockData); - const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + const wrapper = createWrapper(mockPocketBase); - waitFor(() => { - result.current.mutate({ title: 'Test' }); - expect(result.current.isPending).toBe(true); - expect(result.current.isSuccess).toBe(false); - resolveCreate({ id: '1', title: 'Test' }); + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + + const options = { expand: 'relation' }; + result.current.mutate({ title: 'New Item' }, options); + + await waitFor(() => { + expect(mockCreate).toHaveBeenCalledWith({ title: 'New Item' }, options); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); - await createPromise; + it('should handle mutation error', async () => { + const mockError = new Error('Create failed'); + mockCreate.mockRejectedValue(mockError); - return waitFor(() => { - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(true); + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + + result.current.mutate({ title: 'Test' }); + + await waitFor(() => { + expect(result.current.error).toEqual('Create failed'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); }); - }); - it('should throw error when used outside provider', () => { - expect(() => { - renderHook(() => useCreateMutation('test')); - }).toThrow('usePocketBase must be used within a PocketBaseProvider'); + it('should handle non-Error exceptions', async () => { + mockCreate.mockRejectedValue('String error'); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useCreateMutation('test'), { wrapper }); + + result.current.mutate({ title: 'Test' }); + + await waitFor(() => { + expect(result.current.error).toBe('Error creating record'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); + }); }); }); diff --git a/tests/hooks/useDeleteMutation.test.tsx b/tests/hooks/useDeleteMutation.test.tsx index b03c6e3..7bdf99c 100644 --- a/tests/hooks/useDeleteMutation.test.tsx +++ b/tests/hooks/useDeleteMutation.test.tsx @@ -17,111 +17,189 @@ describe('useDeleteMutation', () => { it('should return initial state', () => { const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useDeleteMutation('test'), { wrapper }); + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); expect(result.current.isPending).toBe(false); expect(result.current.error).toBe(null); expect(result.current.isSuccess).toBe(true); + expect(result.current.isError).toBe(false); expect(typeof result.current.mutate).toBe('function'); + expect(typeof result.current.mutateAsync).toBe('function'); }); - it('should handle delete mutation', async () => { - mockDelete.mockResolvedValue(true); + it('should throw error when used outside provider', () => { + expect(() => { + renderHook(() => useDeleteMutation('test', '1')); + }).toThrow('usePocketBase must be used within a PocketBaseProvider'); + }); + it('should handle null id', async () => { const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useDeleteMutation('test'), { wrapper }); + const { result } = renderHook(() => useDeleteMutation('test', null), { wrapper }); + + await expect(result.current.mutateAsync()).rejects.toThrow('ID is required'); + }); + + describe('mutateAsync', () => { + it('should handle successful delete', async () => { + mockDelete.mockResolvedValue(true); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); - const mutationResult = await result.current.mutate('1'); + await result.current.mutateAsync(); - return waitFor(() => { expect(mockDelete).toHaveBeenCalledWith('1', undefined); - expect(mutationResult).toBe(true); expect(result.current.isPending).toBe(false); expect(result.current.isSuccess).toBe(true); }); - }); - it('should handle delete mutation with options', async () => { - mockDelete.mockResolvedValue(true); + it('should handle delete with options', async () => { + mockDelete.mockResolvedValue(true); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useDeleteMutation('test'), { wrapper }); + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); - const options = { headers: { 'X-Custom': 'value' } }; + const options = { headers: { 'X-Custom': 'value' } }; - await result.current.mutate('1', options); - return waitFor(() => { + await result.current.mutateAsync(options); expect(mockDelete).toHaveBeenCalledWith('1', options); }); - }); - it('should handle mutation error', async () => { - const mockError = new Error('Delete failed'); - mockDelete.mockRejectedValue(mockError); + it('should handle mutation error', async () => { + const mockError = new Error('Delete failed'); + mockDelete.mockRejectedValue(mockError); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useDeleteMutation('test'), { wrapper }); + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); - await result.current.mutate('1'); + await expect(result.current.mutateAsync()).rejects.toThrow('Delete failed'); - return waitFor(() => { - expect(result.current.error).toEqual('Delete failed'); - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(false); + await waitFor(() => { + expect(result.current.error).toEqual('Delete failed'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); }); - }); - it('should handle non-Error exceptions', async () => { - mockDelete.mockRejectedValue('String error'); + it('should handle non-Error exceptions', async () => { + mockDelete.mockRejectedValue('String error'); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useDeleteMutation('test'), { wrapper }); + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); - await result.current.mutate('1'); + await expect(result.current.mutateAsync()).rejects.toThrow('Error deleting record'); - return waitFor(() => { - expect(result.current.error).toBe('Error deleting record'); - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(false); + await waitFor(() => { + expect(result.current.error).toBe('Error deleting record'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); }); - }); - it('should set isPending to true during mutation', async () => { - let resolveDelete: (value: unknown) => void; - const deletePromise = new Promise((resolve) => { - resolveDelete = resolve; + it('should set isPending to true during mutation', async () => { + let resolveDelete: (value: unknown) => void = () => {}; + const deletePromise = new Promise((resolve) => { + resolveDelete = resolve; + }); + mockDelete.mockReturnValue(deletePromise); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); + + const mutationPromise = result.current.mutateAsync(); + + await waitFor(() => { + expect(result.current.isPending).toBe(true); + expect(result.current.isSuccess).toBe(false); + }); + + resolveDelete(true); + await mutationPromise; + + await waitFor(() => { + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); - mockDelete.mockReturnValue(deletePromise); + }); - const wrapper = createWrapper(mockPocketBase); + describe('mutate', () => { + it('should handle successful delete', async () => { + mockDelete.mockResolvedValue(true); - const { result } = renderHook(() => useDeleteMutation('test'), { wrapper }); + const wrapper = createWrapper(mockPocketBase); - waitFor(() => { - result.current.mutate('1'); - expect(result.current.isPending).toBe(true); - expect(result.current.isSuccess).toBe(false); + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); + + const mutateResult = result.current.mutate(); + expect(mutateResult).toBeUndefined(); + + await waitFor(() => { + expect(mockDelete).toHaveBeenCalledWith('1', undefined); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); - await waitFor(() => { - resolveDelete(true); + it('should handle delete with options', async () => { + mockDelete.mockResolvedValue(true); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); + + const options = { headers: { 'X-Custom': 'value' } }; + result.current.mutate(options); + + await waitFor(() => { + expect(mockDelete).toHaveBeenCalledWith('1', options); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); - await deletePromise; + it('should handle mutation error', async () => { + const mockError = new Error('Delete failed'); + mockDelete.mockRejectedValue(mockError); - return waitFor(() => { - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(true); + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); + + result.current.mutate(); + + await waitFor(() => { + expect(result.current.error).toEqual('Delete failed'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); }); - }); - it('should throw error when used outside provider', () => { - expect(() => { - renderHook(() => useDeleteMutation('test')); - }).toThrow('usePocketBase must be used within a PocketBaseProvider'); + it('should handle non-Error exceptions', async () => { + mockDelete.mockRejectedValue('String error'); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useDeleteMutation('test', '1'), { wrapper }); + + result.current.mutate(); + + await waitFor(() => { + expect(result.current.error).toBe('Error deleting record'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); + }); }); }); diff --git a/tests/hooks/useRecord.test.tsx b/tests/hooks/useRecord.test.tsx index 1d595c8..0325557 100644 --- a/tests/hooks/useRecord.test.tsx +++ b/tests/hooks/useRecord.test.tsx @@ -424,7 +424,7 @@ describe('useRecord', () => { return Promise.resolve(() => {}); }); - const faultyTransformer = (record: RecordModel) => { + const faultyTransformer = (_record: RecordModel) => { throw new Error('Transformer error'); }; @@ -477,7 +477,7 @@ describe('useRecord', () => { const transformer2 = (record: RecordModel) => ({ ...record, - title: record.title + '!', + title: `${record.title}!`, }); const wrapper = createWrapper(mockPocketBase); diff --git a/tests/hooks/useUpdateMutation.test.tsx b/tests/hooks/useUpdateMutation.test.tsx index d289617..a4e2056 100644 --- a/tests/hooks/useUpdateMutation.test.tsx +++ b/tests/hooks/useUpdateMutation.test.tsx @@ -6,6 +6,7 @@ import { createMockPocketBase, createWrapper, getMockCollectionMethods } from '. describe('useUpdateMutation', () => { let mockPocketBase: PocketBase; + // biome-ignore lint/suspicious/noExplicitAny: Mock function type let mockUpdate: any; beforeEach(() => { @@ -17,27 +18,43 @@ describe('useUpdateMutation', () => { it('should return initial state', () => { const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useUpdateMutation('test'), { wrapper }); + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); expect(result.current.isPending).toBe(false); expect(result.current.error).toBe(null); expect(result.current.isSuccess).toBe(true); + expect(result.current.isError).toBe(false); expect(typeof result.current.mutate).toBe('function'); + expect(typeof result.current.mutateAsync).toBe('function'); }); - it('should handle update mutation', async () => { - const mockData = { id: '1', title: 'Updated Item' }; - mockUpdate.mockResolvedValue(mockData); + it('should throw error when used outside provider', () => { + expect(() => { + renderHook(() => useUpdateMutation('test', '1')); + }).toThrow('usePocketBase must be used within a PocketBaseProvider'); + }); + it('should handle null id', async () => { const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useUpdateMutation('test'), { wrapper }); + const { result } = renderHook(() => useUpdateMutation('test', null), { wrapper }); - const mutationResult = await result.current.mutate('1', { - title: 'Updated Item', - }); + await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('ID is required'); + }); + + describe('mutateAsync', () => { + it('should handle successful update', async () => { + const mockData = { id: '1', title: 'Updated Item' }; + mockUpdate.mockResolvedValue(mockData); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); + + const mutationResult = await result.current.mutateAsync({ + title: 'Updated Item', + }); - return waitFor(() => { expect(mockUpdate).toHaveBeenCalledWith('1', { title: 'Updated Item', }); @@ -45,86 +62,154 @@ describe('useUpdateMutation', () => { expect(result.current.isPending).toBe(false); expect(result.current.isSuccess).toBe(true); }); - }); - it('should handle update mutation with options', async () => { - const mockData = { id: '1', title: 'Updated Item' }; - mockUpdate.mockResolvedValue(mockData); + it('should handle update with options', async () => { + const mockData = { id: '1', title: 'Updated Item' }; + mockUpdate.mockResolvedValue(mockData); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useUpdateMutation('test'), { wrapper }); + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); - const options = { expand: 'relation' }; - await result.current.mutate('1', { title: 'Updated Item' }, options); + const options = { expand: 'relation' }; + await result.current.mutateAsync({ title: 'Updated Item' }, options); - return waitFor(() => { expect(mockUpdate).toHaveBeenCalledWith('1', { title: 'Updated Item' }, options); }); - }); - it('should handle mutation error', async () => { - const mockError = new Error('Update failed'); - mockUpdate.mockRejectedValue(mockError); + it('should handle mutation error', async () => { + const mockError = new Error('Update failed'); + mockUpdate.mockRejectedValue(mockError); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useUpdateMutation('test'), { wrapper }); + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); - await result.current.mutate('1', { title: 'Test' }); + await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('Update failed'); - return waitFor(() => { - expect(result.current.error).toEqual('Update failed'); - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(false); + await waitFor(() => { + expect(result.current.error).toEqual('Update failed'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); }); - }); - it('should handle non-Error exceptions', async () => { - mockUpdate.mockRejectedValue('String error'); + it('should handle non-Error exceptions', async () => { + mockUpdate.mockRejectedValue('String error'); - const wrapper = createWrapper(mockPocketBase); + const wrapper = createWrapper(mockPocketBase); - const { result } = renderHook(() => useUpdateMutation('test'), { wrapper }); + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); - await result.current.mutate('1', { title: 'Test' }); + await expect(result.current.mutateAsync({ title: 'Test' })).rejects.toThrow('Error updating record'); - return waitFor(() => { - expect(result.current.error).toBe('Error updating record'); - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(false); + await waitFor(() => { + expect(result.current.error).toBe('Error updating record'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); + }); + + it('should set isPending to true during mutation', async () => { + let resolveUpdate: (value: unknown) => void = () => {}; + const updatePromise = new Promise((resolve) => { + resolveUpdate = resolve; + }); + mockUpdate.mockReturnValue(updatePromise); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); + + const mutationPromise = result.current.mutateAsync({ title: 'Test' }); + + await waitFor(() => { + expect(result.current.isPending).toBe(true); + expect(result.current.isSuccess).toBe(false); + }); + + resolveUpdate({ id: '1', title: 'Test' }); + await mutationPromise; + + await waitFor(() => { + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); }); - it('should set isPending to true during mutation', async () => { - let resolveUpdate: (value: unknown) => void; - const updatePromise = new Promise((resolve) => { - resolveUpdate = resolve; + describe('mutate', () => { + it('should handle successful update', async () => { + const mockData = { id: '1', title: 'Updated Item' }; + mockUpdate.mockResolvedValue(mockData); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); + + const mutateResult = result.current.mutate({ title: 'Updated Item' }); + expect(mutateResult).toBeUndefined(); + + await waitFor(() => { + expect(mockUpdate).toHaveBeenCalledWith('1', { title: 'Updated Item' }); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); - mockUpdate.mockReturnValue(updatePromise); - const wrapper = createWrapper(mockPocketBase); + it('should handle update with options', async () => { + const mockData = { id: '1', title: 'Updated Item' }; + mockUpdate.mockResolvedValue(mockData); - const { result } = renderHook(() => useUpdateMutation('test'), { wrapper }); + const wrapper = createWrapper(mockPocketBase); - waitFor(() => { - result.current.mutate('1', { title: 'Test' }); - expect(result.current.isPending).toBe(true); - expect(result.current.isSuccess).toBe(false); - resolveUpdate({ id: '1', title: 'Test' }); + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); + + const options = { expand: 'relation' }; + result.current.mutate({ title: 'Updated Item' }, options); + + await waitFor(() => { + expect(mockUpdate).toHaveBeenCalledWith('1', { title: 'Updated Item' }, options); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); }); - await updatePromise; + it('should handle mutation error', async () => { + const mockError = new Error('Update failed'); + mockUpdate.mockRejectedValue(mockError); - return waitFor(() => { - expect(result.current.isPending).toBe(false); - expect(result.current.isSuccess).toBe(true); + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); + + result.current.mutate({ title: 'Test' }); + + await waitFor(() => { + expect(result.current.error).toEqual('Update failed'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); }); - }); - it('should throw error when used outside provider', () => { - expect(() => { - renderHook(() => useUpdateMutation('test')); - }).toThrow('usePocketBase must be used within a PocketBaseProvider'); + it('should handle non-Error exceptions', async () => { + mockUpdate.mockRejectedValue('String error'); + + const wrapper = createWrapper(mockPocketBase); + + const { result } = renderHook(() => useUpdateMutation('test', '1'), { wrapper }); + + result.current.mutate({ title: 'Test' }); + + await waitFor(() => { + expect(result.current.error).toBe('Error updating record'); + expect(result.current.isPending).toBe(false); + expect(result.current.isSuccess).toBe(false); + expect(result.current.isError).toBe(true); + }); + }); }); });