From cd1a4f6841af4acfe84ee0e9a879e16de39ec44a Mon Sep 17 00:00:00 2001 From: imeepos Date: Tue, 27 Jan 2026 17:18:54 +0800 Subject: [PATCH] feat: update RefreshControl mocks in tests and improve error handling in useChangePassword hook --- app/(tabs)/__tests__/index.test.tsx | 8 +++++++- app/(tabs)/video.test.tsx | 8 +++++++- app/generationRecord.test.tsx | 10 ++++++---- app/searchResults.tsx | 2 +- components/blocks/home/HeroSlider.test.tsx | 4 ++-- components/blocks/home/HeroSlider.tsx | 4 ++-- components/blocks/home/TabNavigation.tsx | 2 +- components/blocks/home/TemplateCard.tsx | 4 ++-- components/blocks/home/TitleBar.tsx | 2 +- hooks/use-change-password.ts | 10 +++++----- hooks/use-templates.ts | 2 +- hooks/use-works-search.ts | 4 +++- tests/hooks/use-works-search.test.ts | 22 +++++++++++----------- 13 files changed, 49 insertions(+), 33 deletions(-) diff --git a/app/(tabs)/__tests__/index.test.tsx b/app/(tabs)/__tests__/index.test.tsx index 8e752e1..ec6f7ac 100644 --- a/app/(tabs)/__tests__/index.test.tsx +++ b/app/(tabs)/__tests__/index.test.tsx @@ -28,7 +28,13 @@ jest.mock('@/components/icon', () => ({ // Mock components jest.mock('@/components/ErrorState', () => 'ErrorState') jest.mock('@/components/LoadingState', () => 'LoadingState') -jest.mock('@/components/RefreshControl', () => 'RefreshControl') + +// Mock react-native RefreshControl (directly from react-native) +jest.mock('react-native', () => + Object.assign({}, jest.requireActual('react-native'), { + RefreshControl: 'RefreshControl', + }) +) // Mock dependencies jest.mock('expo-router', () => ({ diff --git a/app/(tabs)/video.test.tsx b/app/(tabs)/video.test.tsx index d9d36c8..0bb72a4 100644 --- a/app/(tabs)/video.test.tsx +++ b/app/(tabs)/video.test.tsx @@ -44,9 +44,15 @@ jest.mock('@/components/icon', () => ({ // Mock UI components jest.mock('@/components/LoadingState', () => 'LoadingState') jest.mock('@/components/ErrorState', () => 'ErrorState') -jest.mock('@/components/RefreshControl', () => 'RefreshControl') jest.mock('@/components/PaginationLoader', () => 'PaginationLoader') +// Mock react-native RefreshControl (directly from react-native) +jest.mock('react-native', () => + Object.assign({}, jest.requireActual('react-native'), { + RefreshControl: 'RefreshControl', + }) +) + // Mock hooks jest.mock('@/hooks', () => ({ useTemplates: jest.fn(() => ({ diff --git a/app/generationRecord.test.tsx b/app/generationRecord.test.tsx index 528bc46..853be80 100644 --- a/app/generationRecord.test.tsx +++ b/app/generationRecord.test.tsx @@ -57,10 +57,12 @@ jest.mock('@/components/PaginationLoader', () => ({ default: ({ testID }: any) => , })) -jest.mock('@/components/RefreshControl', () => ({ - __esModule: true, - default: () => null, -})) +// Mock react-native RefreshControl (directly from react-native) +jest.mock('react-native', () => + Object.assign({}, jest.requireActual('react-native'), { + RefreshControl: ({ children }: { children: React.ReactNode }) => <>{children}, + }) +) jest.mock('@/hooks', () => ({ useTemplateGenerations: jest.fn(), diff --git a/app/searchResults.tsx b/app/searchResults.tsx index 6933da7..75689e0 100644 --- a/app/searchResults.tsx +++ b/app/searchResults.tsx @@ -56,7 +56,7 @@ export default function SearchResultsScreen() { aspectRatio: template.aspectRatio ? parseFloat(template.aspectRatio as string) : undefined, })) - if (loading && !refreshing) { + if (loading) { return ( diff --git a/components/blocks/home/HeroSlider.test.tsx b/components/blocks/home/HeroSlider.test.tsx index 5f8b521..d43f263 100644 --- a/components/blocks/home/HeroSlider.test.tsx +++ b/components/blocks/home/HeroSlider.test.tsx @@ -80,7 +80,7 @@ describe('HeroSlider Component', () => { describe('Callback Behavior', () => { it('should call onActivityPress with link when activity is pressed', () => { const onActivityPress = jest.fn() - const props = { + const props: { activities: typeof mockActivities; onActivityPress?: (link: string) => void } = { activities: mockActivities, onActivityPress, } @@ -90,7 +90,7 @@ describe('HeroSlider Component', () => { }) it('should not throw when onActivityPress is not provided', () => { - const props = { activities: mockActivities } + const props: { activities: typeof mockActivities; onActivityPress?: (link: string) => void } = { activities: mockActivities } expect(() => { if (props.onActivityPress) { props.onActivityPress(mockActivities[0].link) diff --git a/components/blocks/home/HeroSlider.tsx b/components/blocks/home/HeroSlider.tsx index bb2fb32..c9769a8 100644 --- a/components/blocks/home/HeroSlider.tsx +++ b/components/blocks/home/HeroSlider.tsx @@ -2,7 +2,7 @@ import React from 'react' import { View, Text, Pressable, ScrollView, StyleSheet } from 'react-native' import { Image } from 'expo-image' -interface Activity { +export interface Activity { id: string title: string titleEn?: string @@ -20,7 +20,7 @@ interface HeroSliderProps { export function HeroSlider({ activities, onActivityPress, -}: HeroSliderProps): JSX.Element | null { +}: HeroSliderProps): React.ReactNode | null { // 空数据时返回 null if (!activities || activities.length === 0) { return null diff --git a/components/blocks/home/TabNavigation.tsx b/components/blocks/home/TabNavigation.tsx index cb66698..94b48bd 100644 --- a/components/blocks/home/TabNavigation.tsx +++ b/components/blocks/home/TabNavigation.tsx @@ -35,7 +35,7 @@ export function TabNavigation({ isSticky = false, wrapperStyle, onLayout, -}: TabNavigationProps): JSX.Element { +}: TabNavigationProps): React.ReactNode { const scrollViewRef = useRef(null) const tabLayouts = useRef<{ x: number; width: number }[]>([]) diff --git a/components/blocks/home/TemplateCard.tsx b/components/blocks/home/TemplateCard.tsx index b06cd65..f49b4c0 100644 --- a/components/blocks/home/TemplateCard.tsx +++ b/components/blocks/home/TemplateCard.tsx @@ -1,5 +1,5 @@ import React, { memo, useCallback, useMemo } from 'react' -import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native' +import { Pressable, StyleSheet, Text, View, ViewStyle, ImageStyle } from 'react-native' import { Image } from 'expo-image' import { LinearGradient } from 'expo-linear-gradient' @@ -74,7 +74,7 @@ const TemplateCardComponent: React.FC = ({ [aspectRatio] ) const imageStyle = useMemo(() => - [styles.cardImage, aspectRatio ? { aspectRatio } : undefined].filter(Boolean) as ViewStyle[], + [styles.cardImage, aspectRatio ? { aspectRatio } : undefined].filter(Boolean) as ImageStyle[], [aspectRatio] ) diff --git a/components/blocks/home/TitleBar.tsx b/components/blocks/home/TitleBar.tsx index 34dfe81..048a85d 100644 --- a/components/blocks/home/TitleBar.tsx +++ b/components/blocks/home/TitleBar.tsx @@ -14,7 +14,7 @@ export function TitleBar({ onPointsPress, onSearchPress, onLayout, -}: TitleBarProps): JSX.Element { +}: TitleBarProps): React.ReactNode { return ( { // 客户端验证 if (!oldPassword) { - setError({ message: '旧密码不能为空' }) + setError({ message: '旧密码不能为空', status: 400, statusText: 'Bad Request' }) return } if (newPassword.length < 6) { - setError({ message: '新密码长度至少为6位' }) + setError({ message: '新密码长度至少为6位', status: 400, statusText: 'Bad Request' }) return } if (oldPassword === newPassword) { - setError({ message: '新密码不能与当前密码相同' }) + setError({ message: '新密码不能与当前密码相同', status: 400, statusText: 'Bad Request' }) return } if (confirmPassword !== undefined && newPassword !== confirmPassword) { - setError({ message: '新密码和确认密码不一致' }) + setError({ message: '新密码和确认密码不一致', status: 400, statusText: 'Bad Request' }) return } @@ -49,7 +49,7 @@ export const useChangePassword = (): ChangePasswordResult => { const result = await handleError(async () => { return await authClient.changePassword({ - oldPassword, + currentPassword: oldPassword, newPassword, revokeOtherSessions: true, }) diff --git a/hooks/use-templates.ts b/hooks/use-templates.ts index 761aa71..c40e1be 100644 --- a/hooks/use-templates.ts +++ b/hooks/use-templates.ts @@ -8,7 +8,7 @@ import { OWNER_ID } from '@/lib/auth' import { handleError } from './use-error' -type ListTemplatesParams = Omit +type ListTemplatesParams = Partial> const DEFAULT_PARAMS = { limit: 20, diff --git a/hooks/use-works-search.ts b/hooks/use-works-search.ts index f8edecc..68c2ce4 100644 --- a/hooks/use-works-search.ts +++ b/hooks/use-works-search.ts @@ -17,9 +17,11 @@ import { type ApiError } from '@/lib/types' import { handleError } from './use-error' // Type definitions +export type WorksCategory = '全部' | '萌宠' | '写真' | '合拍' + export interface UseWorksSearchParams { keyword: string - category?: '全部' | '萌宠' | '写真' | '合拍' + category?: WorksCategory page?: number limit?: number } diff --git a/tests/hooks/use-works-search.test.ts b/tests/hooks/use-works-search.test.ts index c277a36..6d8ba1e 100644 --- a/tests/hooks/use-works-search.test.ts +++ b/tests/hooks/use-works-search.test.ts @@ -8,7 +8,7 @@ */ import { renderHook, waitFor, act } from '@testing-library/react-native' -import { useWorksSearch } from '@/hooks/use-works-search' +import { useWorksSearch, type WorksCategory } from '@/hooks/use-works-search' import { root } from '@repo/core' import { TemplateGenerationController } from '@repo/sdk' @@ -21,15 +21,15 @@ jest.mock('@repo/core', () => ({ // Mock @tanstack/react-query before importing the hook const mockRefetch = jest.fn() -const mockUseQuery = jest.fn(() => ({ - data: undefined, - isLoading: false, - error: null, - refetch: mockRefetch, -})) +const mockUseQuery = jest.fn() + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockUseQueryImpl = (...args: any[]) => { + return mockUseQuery(args[0], args[1]) +} jest.mock('@tanstack/react-query', () => ({ - useQuery: jest.fn((args) => mockUseQuery(args)), + useQuery: mockUseQueryImpl, })) describe('useWorksSearch', () => { @@ -267,16 +267,16 @@ describe('useWorksSearch', () => { }) const { result, rerender } = renderHook( - ({ keyword, category }) => useWorksSearch({ keyword, category }), + ({ keyword, category }: { keyword: string; category?: WorksCategory }) => useWorksSearch({ keyword, category }), { - initialProps: { keyword: '测试', category: '萌宠' as const }, + initialProps: { keyword: '测试', category: '萌宠' }, } ) expect(result.current.works).toEqual(mockData1.data) // Switch category - rerender({ keyword: '测试', category: '写真' as const }) + rerender({ keyword: '测试', category: '写真' }) expect(result.current.works).toEqual(mockData2.data) })