From 7ebd225976095876eae47f829b0ebfe80be0839c Mon Sep 17 00:00:00 2001 From: imeepos Date: Wed, 21 Jan 2026 11:57:17 +0800 Subject: [PATCH] test: add comprehensive tests for use-template-detail hook Add complete test coverage for use-template-detail hook including: - Initial state verification - Execute function with success and error cases - Loading state management - Refetch functionality - Error handling and clearing All tests passing (10/10). Hook already has complete loading states and refetch functionality as documented in REVIEW.md. Co-Authored-By: Claude Opus 4.5 --- hooks/use-template-detail.test.ts | 259 ++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 hooks/use-template-detail.test.ts diff --git a/hooks/use-template-detail.test.ts b/hooks/use-template-detail.test.ts new file mode 100644 index 0000000..46b04c5 --- /dev/null +++ b/hooks/use-template-detail.test.ts @@ -0,0 +1,259 @@ +import { renderHook, act } from '@testing-library/react-native' +import { useTemplateDetail } from './use-template-detail' +import { root } from '@repo/core' +import { TemplateController } from '@repo/sdk' +import { handleError } from './use-error' + +jest.mock('@repo/core', () => ({ + root: { + get: jest.fn(), + }, +})) + +jest.mock('./use-error', () => ({ + handleError: jest.fn(async (cb) => { + try { + const data = await cb() + return { data, error: null } + } catch (e) { + return { data: null, error: e } + } + }), +})) + +describe('useTemplateDetail', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + describe('initial state', () => { + it('should return initial state with no data', () => { + const { result } = renderHook(() => useTemplateDetail()) + + expect(result.current.data).toBeUndefined() + expect(result.current.loading).toBe(false) + expect(result.current.error).toBeNull() + }) + }) + + describe('execute function', () => { + it('should load template detail successfully', async () => { + const mockData = { + id: 'template-1', + name: 'Test Template', + description: 'Test Description', + } + + const mockController = { + get: jest.fn().mockResolvedValue(mockData), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + await act(async () => { + await result.current.execute({ id: 'template-1' }) + }) + + expect(mockController.get).toHaveBeenCalledWith('template-1') + expect(result.current.data).toEqual(mockData) + expect(result.current.error).toBeNull() + }) + + it('should handle API errors', async () => { + const mockError = { + status: 404, + statusText: 'Not Found', + message: 'Template not found', + } + + const mockController = { + get: jest.fn().mockRejectedValue(mockError), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + await act(async () => { + await result.current.execute({ id: 'invalid-id' }) + }) + + expect(result.current.error).toEqual(mockError) + expect(result.current.data).toBeUndefined() + }) + + it('should return error in response when API fails', async () => { + const mockError = { + status: 500, + statusText: 'Internal Server Error', + message: 'Server error', + } + + const mockController = { + get: jest.fn().mockRejectedValue(mockError), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + let response + await act(async () => { + response = await result.current.execute({ id: 'template-1' }) + }) + + expect(response).toEqual({ data: undefined, error: mockError }) + }) + + it('should return data in response when API succeeds', async () => { + const mockData = { + id: 'template-1', + name: 'Test Template', + } + + const mockController = { + get: jest.fn().mockResolvedValue(mockData), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + let response + await act(async () => { + response = await result.current.execute({ id: 'template-1' }) + }) + + expect(response).toEqual({ data: mockData, error: null }) + }) + }) + + describe('loading state', () => { + it('should set loading to true during fetch', async () => { + let resolveFetch: (value: any) => void + const fetchPromise = new Promise((resolve) => { + resolveFetch = resolve + }) + + const mockController = { + get: jest.fn().mockReturnValue(fetchPromise), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + act(() => { + result.current.execute({ id: 'template-1' }) + }) + + expect(result.current.loading).toBe(true) + + await act(async () => { + resolveFetch!({ id: 'template-1', name: 'Test' }) + await fetchPromise + }) + + expect(result.current.loading).toBe(false) + }) + + it('should set loading to false after error', async () => { + const mockError = { status: 500, statusText: 'Error', message: 'Error' } + + const mockController = { + get: jest.fn().mockRejectedValue(mockError), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + await act(async () => { + await result.current.execute({ id: 'template-1' }) + }) + + expect(result.current.loading).toBe(false) + }) + }) + + describe('refetch function', () => { + it('should reload template data', async () => { + const initialData = { + id: 'template-1', + name: 'Initial Name', + } + + const refreshedData = { + id: 'template-1', + name: 'Updated Name', + } + + const mockController = { + get: jest.fn() + .mockResolvedValueOnce(initialData) + .mockResolvedValueOnce(refreshedData), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + await act(async () => { + await result.current.execute({ id: 'template-1' }) + }) + + expect(result.current.data).toEqual(initialData) + + await act(async () => { + await result.current.refetch({ id: 'template-1' }) + }) + + expect(result.current.data).toEqual(refreshedData) + expect(mockController.get).toHaveBeenCalledTimes(2) + }) + + it('should call execute with same params', async () => { + const mockData = { id: 'template-1', name: 'Test' } + + const mockController = { + get: jest.fn().mockResolvedValue(mockData), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + await act(async () => { + await result.current.refetch({ id: 'template-1' }) + }) + + expect(mockController.get).toHaveBeenCalledWith('template-1') + }) + }) + + describe('error handling', () => { + it('should clear error on successful execute', async () => { + const mockError = { status: 500, statusText: 'Error', message: 'Error' } + const mockData = { id: 'template-1', name: 'Test' } + + const mockController = { + get: jest.fn() + .mockRejectedValueOnce(mockError) + .mockResolvedValueOnce(mockData), + } + ;(root.get as jest.Mock).mockReturnValue(mockController) + + const { result } = renderHook(() => useTemplateDetail()) + + await act(async () => { + await result.current.execute({ id: 'template-1' }) + }) + + expect(result.current.error).toEqual(mockError) + + await act(async () => { + await result.current.execute({ id: 'template-1' }) + }) + + expect(result.current.error).toBeNull() + }) + }) +})