222 lines
5.6 KiB
TypeScript
222 lines
5.6 KiB
TypeScript
|
|
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');
|
||
|
|
});
|
||
|
|
});
|