Files
life-echo/app-expo/tests/features/profile/hooks.test.tsx

222 lines
5.6 KiB
TypeScript
Raw Normal View History

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 (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
}
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(
'<html><body><h1>用户协议</h1></body></html>',
);
const { result } = renderHook(() => useLegalDoc('terms'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toContain('<h1>用户协议</h1>');
expect(mockFetchLegalDoc).toHaveBeenCalledWith('terms');
});
test('fetches privacy HTML', async () => {
mockFetchLegalDoc.mockResolvedValue(
'<html><body><h1>隐私政策</h1></body></html>',
);
const { result } = renderHook(() => useLegalDoc('privacy'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockFetchLegalDoc).toHaveBeenCalledWith('privacy');
});
});