import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook, waitFor, act } from '@testing-library/react-native'; import React, { type PropsWithChildren } from 'react'; import { useProfile, useUpdateProfile, useCurrentPlan, useQuota, useSubmitFeedback, useLegalDoc, } from '@/features/profile/hooks'; // ─── Mocks ─── const mockFetchProfile = jest.fn(); const mockUpdateProfile = jest.fn(); const mockFetchCurrentPlan = jest.fn(); const mockCheckQuota = jest.fn(); const mockSubmitFeedback = jest.fn(); const mockFetchLegalDoc = jest.fn(); jest.mock('@/features/profile/api', () => ({ profileApi: { fetchProfile: () => mockFetchProfile(), updateProfile: (body: unknown) => mockUpdateProfile(body), fetchPlans: jest.fn(), fetchCurrentPlan: () => mockFetchCurrentPlan(), checkQuota: () => mockCheckQuota(), submitFeedback: (body: unknown) => mockSubmitFeedback(body), fetchLegalDoc: (type: string) => mockFetchLegalDoc(type), }, })); // ─── Helpers ─── let queryClient: QueryClient; function createWrapper() { queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity }, mutations: { retry: false }, }, }); return function Wrapper({ children }: PropsWithChildren) { return ( {children} ); }; } afterEach(async () => { await queryClient?.cancelQueries(); queryClient?.clear(); jest.clearAllMocks(); }); const fakeProfile = { id: 'u1', phone: '13800138000', email: null, nickname: 'Test', avatar_url: null, subscription_type: 'free', created_at: '2026-01-01T00:00:00Z', birth_year: 1990, birth_place: '北京', grew_up_place: '上海', occupation: '工程师', }; // ─── Tests ─── describe('useProfile', () => { test('fetches user profile', async () => { mockFetchProfile.mockResolvedValue(fakeProfile); const { result } = renderHook(() => useProfile(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toEqual(fakeProfile); }); }); describe('useUpdateProfile', () => { test('updates profile and sets cache on success', async () => { const updated = { ...fakeProfile, occupation: '设计师' }; mockUpdateProfile.mockResolvedValue(updated); const wrapper = createWrapper(); const { result } = renderHook(() => useUpdateProfile(), { wrapper }); await act(async () => { result.current.mutate({ occupation: '设计师' }); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(mockUpdateProfile).toHaveBeenCalledWith({ occupation: '设计师' }); }); }); describe('useCurrentPlan', () => { test('fetches current plan', async () => { const plan = { plan_id: 'free', plan_name: '免费版', subscription_type: 'free', expires_at: null, features: ['基础对话'], usage: { conversations: 2, chapters: 1, max_conversations: 5, max_chapters: 3, }, }; mockFetchCurrentPlan.mockResolvedValue(plan); const { result } = renderHook(() => useCurrentPlan(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.plan_name).toBe('免费版'); }); }); describe('useQuota', () => { test('fetches quota check', async () => { const quota = { has_quota: true, remaining_conversations: 3, remaining_chapters: 2, remaining_words: null, used_conversations: 2, used_chapters: 1, max_conversations: 5, max_chapters: 3, message: 'OK', }; mockCheckQuota.mockResolvedValue(quota); const { result } = renderHook(() => useQuota(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.has_quota).toBe(true); expect(result.current.data?.remaining_conversations).toBe(3); }); }); describe('useSubmitFeedback', () => { test('submits feedback', async () => { mockSubmitFeedback.mockResolvedValue({ id: 'fb1', message: '感谢反馈' }); const wrapper = createWrapper(); const { result } = renderHook(() => useSubmitFeedback(), { wrapper }); await act(async () => { result.current.mutate({ content: '很好用' }); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(mockSubmitFeedback).toHaveBeenCalledWith({ content: '很好用' }); }); }); describe('useLegalDoc', () => { test('fetches terms HTML', async () => { mockFetchLegalDoc.mockResolvedValue( '

用户协议

', ); const { result } = renderHook(() => useLegalDoc('terms'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toContain('

用户协议

'); expect(mockFetchLegalDoc).toHaveBeenCalledWith('terms'); }); test('fetches privacy HTML', async () => { mockFetchLegalDoc.mockResolvedValue( '

隐私政策

', ); const { result } = renderHook(() => useLegalDoc('privacy'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(mockFetchLegalDoc).toHaveBeenCalledWith('privacy'); }); });