Squash merge feat/expo-app: app-expo, .cursor, workflows, package.json, .husky; remove app-android, app-ios, react-app
This commit is contained in:
250
app-expo/tests/features/profile/hooks.test.tsx
Normal file
250
app-expo/tests/features/profile/hooks.test.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
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,
|
||||
useFaqs,
|
||||
useSubmitFeedback,
|
||||
useLegalDoc,
|
||||
} from '@/features/profile/hooks';
|
||||
|
||||
// ─── Mocks ───
|
||||
|
||||
const mockFetchProfile = jest.fn();
|
||||
const mockUpdateProfile = jest.fn();
|
||||
const mockFetchCurrentPlan = jest.fn();
|
||||
const mockCheckQuota = jest.fn();
|
||||
const mockFetchFaqs = 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(),
|
||||
fetchFaqs: () => mockFetchFaqs(),
|
||||
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('useFaqs', () => {
|
||||
test('fetches FAQ list', async () => {
|
||||
const faqs = [
|
||||
{
|
||||
id: 'f1',
|
||||
question: '如何使用?',
|
||||
answer: '点击开始',
|
||||
category: 'general',
|
||||
order: 1,
|
||||
},
|
||||
];
|
||||
mockFetchFaqs.mockResolvedValue(faqs);
|
||||
|
||||
const { result } = renderHook(() => useFaqs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toHaveLength(1);
|
||||
expect(result.current.data?.[0].question).toBe('如何使用?');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user