From 7c3b3c066c03d2cb2423a3b5ac0b8f12720ae55f Mon Sep 17 00:00:00 2001 From: imeepos Date: Thu, 29 Jan 2026 16:00:21 +0800 Subject: [PATCH] fix: bug --- app/(tabs)/__tests__/message.test.tsx | 151 +++++++++++++++++++++++++- app/(tabs)/message.tsx | 31 ++++-- components/message/MessageCard.tsx | 13 --- components/message/SwipeToDelete.tsx | 3 + hooks/use-messages.ts | 9 +- 5 files changed, 183 insertions(+), 24 deletions(-) diff --git a/app/(tabs)/__tests__/message.test.tsx b/app/(tabs)/__tests__/message.test.tsx index 4a6722d..b1ccef6 100644 --- a/app/(tabs)/__tests__/message.test.tsx +++ b/app/(tabs)/__tests__/message.test.tsx @@ -23,6 +23,10 @@ jest.mock('react-i18next', () => ({ // Mock expo-router const mockPush = jest.fn() +const mockUseFocusEffect = jest.fn((callback: () => void) => { + // 立即执行 callback 来模拟页面获得焦点 + callback() +}) jest.mock('expo-router', () => ({ useRouter: () => ({ push: mockPush, @@ -30,6 +34,7 @@ jest.mock('expo-router', () => ({ back: jest.fn(), }), useLocalSearchParams: () => ({}), + useFocusEffect: (callback: () => void) => mockUseFocusEffect(callback), })) // Mock hooks @@ -87,6 +92,7 @@ jest.mock('@/hooks/use-messages', () => ({ messages: mockMessages, loading: false, loadingMore: false, + refreshing: false, error: null, execute: mockExecute, refetch: mockRefetch, @@ -524,9 +530,10 @@ describe('MessageScreen', () => { }) describe('Initial Data Fetch', () => { - it('should call execute on mount', () => { + it('should call refetch on mount via useFocusEffect', () => { render() - expect(mockExecute).toHaveBeenCalled() + // 现在使用 useFocusEffect + refetch 替代 useEffect + execute + expect(mockRefetch).toHaveBeenCalled() }) it('should call announcement execute on mount', () => { @@ -534,4 +541,144 @@ describe('MessageScreen', () => { expect(mockAnnouncementExecute).toHaveBeenCalled() }) }) + + describe('Pull-to-Refresh', () => { + beforeEach(() => { + jest.clearAllMocks() + const { useMessages } = require('@/hooks/use-messages') + useMessages.mockReturnValue({ + messages: mockMessages, + loading: false, + loadingMore: false, + refreshing: false, + error: null, + execute: mockExecute, + refetch: mockRefetch, + loadMore: mockLoadMore, + hasMore: true, + }) + + const { useAnnouncements } = require('@/hooks/use-announcements') + useAnnouncements.mockReturnValue({ + announcements: mockAnnouncements, + loading: false, + error: null, + execute: mockAnnouncementExecute, + refetch: jest.fn(), + loadMore: jest.fn(), + hasMore: false, + }) + }) + + it('should render FlatList with RefreshControl', () => { + const { getByTestId } = render() + const flatList = getByTestId('message-list') + expect(flatList.props.refreshControl).toBeTruthy() + }) + + it('should call refetch when pull-to-refresh is triggered', async () => { + const { getByTestId } = render() + const flatList = getByTestId('message-list') + + // 触发 onRefresh + const refreshControl = flatList.props.refreshControl + refreshControl.props.onRefresh() + + await waitFor(() => { + expect(mockRefetch).toHaveBeenCalled() + }) + }) + + it('should call executeAnnouncements when pull-to-refresh is triggered', async () => { + const { getByTestId } = render() + const flatList = getByTestId('message-list') + + // 触发 onRefresh + const refreshControl = flatList.props.refreshControl + refreshControl.props.onRefresh() + + await waitFor(() => { + expect(mockAnnouncementExecute).toHaveBeenCalled() + }) + }) + + it('should show refreshing indicator when refreshing is true', () => { + const { useMessages } = require('@/hooks/use-messages') + useMessages.mockReturnValue({ + messages: mockMessages, + loading: false, + loadingMore: false, + refreshing: true, + error: null, + execute: mockExecute, + refetch: mockRefetch, + loadMore: mockLoadMore, + hasMore: true, + }) + + const { getByTestId } = render() + const flatList = getByTestId('message-list') + const refreshControl = flatList.props.refreshControl + + expect(refreshControl.props.refreshing).toBe(true) + }) + + it('should not show refreshing indicator when refreshing is false', () => { + const { getByTestId } = render() + const flatList = getByTestId('message-list') + const refreshControl = flatList.props.refreshControl + + expect(refreshControl.props.refreshing).toBe(false) + }) + }) + + describe('Tab Focus Reload', () => { + beforeEach(() => { + jest.clearAllMocks() + const { useMessages } = require('@/hooks/use-messages') + useMessages.mockReturnValue({ + messages: mockMessages, + loading: false, + loadingMore: false, + refreshing: false, + error: null, + execute: mockExecute, + refetch: mockRefetch, + loadMore: mockLoadMore, + hasMore: true, + }) + + const { useAnnouncements } = require('@/hooks/use-announcements') + useAnnouncements.mockReturnValue({ + announcements: mockAnnouncements, + loading: false, + error: null, + execute: mockAnnouncementExecute, + refetch: jest.fn(), + loadMore: jest.fn(), + hasMore: false, + }) + }) + + it('should call refetch when tab gains focus', () => { + render() + + // useFocusEffect 在 mock 中会立即执行 callback + // 所以 refetch 应该被调用 + expect(mockRefetch).toHaveBeenCalled() + }) + + it('should call executeAnnouncements when tab gains focus', () => { + render() + + expect(mockAnnouncementExecute).toHaveBeenCalled() + }) + + it('should use useFocusEffect hook', () => { + render() + + // 验证 useFocusEffect 被调用 + expect(mockUseFocusEffect).toHaveBeenCalled() + }) + }) }) diff --git a/app/(tabs)/message.tsx b/app/(tabs)/message.tsx index ab2e86e..64abae3 100644 --- a/app/(tabs)/message.tsx +++ b/app/(tabs)/message.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useCallback } from 'react' import { View, Text, @@ -6,11 +6,12 @@ import { FlatList, StatusBar as RNStatusBar, Pressable, + RefreshControl, } from 'react-native' import { StatusBar } from 'expo-status-bar' import { SafeAreaView } from 'react-native-safe-area-context' import { useTranslation } from 'react-i18next' -import { useRouter } from 'expo-router' +import { useRouter, useFocusEffect } from 'expo-router' import { useMessages, type Message } from '@/hooks/use-messages' import { useMessageActions } from '@/hooks/use-message-actions' @@ -54,11 +55,11 @@ export default function MessageScreen() { messages: allMessages, loading, loadingMore, + refreshing, error, refetch, loadMore, hasMore, - execute, } = useMessages({ limit: 20 }) const { @@ -75,11 +76,19 @@ export default function MessageScreen() { // Filter messages based on active tab const filteredMessages = filterMessagesByTab(allMessages, activeTab) - // Initial data fetch - useEffect(() => { - execute() + // Reload data when tab gains focus + useFocusEffect( + useCallback(() => { + refetch() + executeAnnouncements() + }, [refetch, executeAnnouncements]) + ) + + // Handle pull-to-refresh + const handleRefresh = useCallback(() => { + refetch() executeAnnouncements() - }, []) + }, [refetch, executeAnnouncements]) // Handle tab change const handleTabChange = useCallback((type: MessageType) => { @@ -242,6 +251,14 @@ export default function MessageScreen() { onEndReachedThreshold={0.5} ListFooterComponent={renderFooter} ListEmptyComponent={renderEmptyComponent} + refreshControl={ + + } /> ) diff --git a/components/message/MessageCard.tsx b/components/message/MessageCard.tsx index abab8a5..3680b89 100644 --- a/components/message/MessageCard.tsx +++ b/components/message/MessageCard.tsx @@ -141,19 +141,6 @@ export const MessageCard: React.FC = ({ )} - {/* Left: Icon or Avatar */} - - {showAvatar ? ( - - ) : ( - {icon} - )} - - {/* Middle: Content */} {message.title} diff --git a/components/message/SwipeToDelete.tsx b/components/message/SwipeToDelete.tsx index 0e6832d..23d57f5 100644 --- a/components/message/SwipeToDelete.tsx +++ b/components/message/SwipeToDelete.tsx @@ -66,6 +66,9 @@ const styles = StyleSheet.create({ width: 80, justifyContent: 'center', alignItems: 'center', + marginBottom: 12, + borderTopRightRadius: 16, + borderBottomRightRadius: 16, }, deleteText: { color: '#FFFFFF', diff --git a/hooks/use-messages.ts b/hooks/use-messages.ts index 7b64eb6..ec653ae 100644 --- a/hooks/use-messages.ts +++ b/hooks/use-messages.ts @@ -22,6 +22,7 @@ const DEFAULT_PARAMS = { export const useMessages = (initialParams?: ListMessagesParams) => { const [loading, setLoading] = useState(false) const [loadingMore, setLoadingMore] = useState(false) + const [refreshing, setRefreshing] = useState(false) const [error, setError] = useState(null) const [data, setData] = useState() const currentPageRef = useRef(1) @@ -93,9 +94,12 @@ export const useMessages = (initialParams?: ListMessagesParams) => { return { data: newData, error: null } }, [loading, loadingMore, initialParams]) - const refetch = useCallback(() => { + const refetch = useCallback(async () => { hasMoreRef.current = true - return execute() + setRefreshing(true) + const result = await execute() + setRefreshing(false) + return result }, [execute]) return { @@ -103,6 +107,7 @@ export const useMessages = (initialParams?: ListMessagesParams) => { messages: data?.messages || [], loading, loadingMore, + refreshing, error, execute, refetch,